@cleocode/contracts 2026.5.104 → 2026.5.106
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/boundary.js +5 -5
- package/dist/boundary.js.map +1 -1
- package/dist/errors.d.ts +61 -0
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +46 -0
- package/dist/errors.js.map +1 -1
- package/dist/exit-codes.d.ts +13 -0
- package/dist/exit-codes.d.ts.map +1 -1
- package/dist/exit-codes.js +13 -0
- package/dist/exit-codes.js.map +1 -1
- package/dist/index.d.ts +5 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -1
- package/dist/index.js.map +1 -1
- package/dist/release/ship-e2e-smoke.d.ts +97 -0
- package/dist/release/ship-e2e-smoke.d.ts.map +1 -0
- package/dist/release/ship-e2e-smoke.js +20 -0
- package/dist/release/ship-e2e-smoke.js.map +1 -0
- package/dist/render/__tests__/envelope.test.d.ts +14 -0
- package/dist/render/__tests__/envelope.test.d.ts.map +1 -0
- package/dist/render/__tests__/envelope.test.js +178 -0
- package/dist/render/__tests__/envelope.test.js.map +1 -0
- package/dist/render/__tests__/icon.test.d.ts +2 -0
- package/dist/render/__tests__/icon.test.d.ts.map +1 -0
- package/dist/render/__tests__/icon.test.js +124 -0
- package/dist/render/__tests__/icon.test.js.map +1 -0
- package/dist/render/envelope.d.ts +103 -0
- package/dist/render/envelope.d.ts.map +1 -0
- package/dist/render/envelope.js +53 -0
- package/dist/render/envelope.js.map +1 -0
- package/dist/render/icon.d.ts +120 -0
- package/dist/render/icon.d.ts.map +1 -0
- package/dist/render/icon.js +163 -0
- package/dist/render/icon.js.map +1 -0
- package/dist/render/index.d.ts +16 -0
- package/dist/render/index.d.ts.map +1 -0
- package/dist/render/index.js +12 -0
- package/dist/render/index.js.map +1 -0
- package/dist/render/list.d.ts +77 -0
- package/dist/render/list.d.ts.map +1 -0
- package/dist/render/list.js +39 -0
- package/dist/render/list.js.map +1 -0
- package/dist/render/table.d.ts +73 -0
- package/dist/render/table.d.ts.map +1 -0
- package/dist/render/table.js +32 -0
- package/dist/render/table.js.map +1 -0
- package/dist/render/tree.d.ts +94 -0
- package/dist/render/tree.d.ts.map +1 -0
- package/dist/render/tree.js +30 -0
- package/dist/render/tree.js.map +1 -0
- package/package.json +2 -2
- package/src/boundary.ts +5 -5
- package/src/errors.ts +71 -0
- package/src/exit-codes.ts +13 -0
- package/src/index.ts +15 -1
- package/src/release/ship-e2e-smoke.ts +113 -0
- package/src/render/__tests__/envelope.test.ts +194 -0
- package/src/render/__tests__/icon.test.ts +138 -0
- package/src/render/envelope.ts +112 -0
- package/src/render/icon.ts +182 -0
- package/src/render/index.ts +40 -0
- package/src/render/list.ts +90 -0
- package/src/render/table.ts +85 -0
- package/src/render/tree.ts +114 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tree.d.ts","sourceRoot":"","sources":["../../src/render/tree.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH;;;;GAIG;AACH,MAAM,MAAM,YAAY,GAAG,MAAM,GAAG,MAAM,GAAG,MAAM,GAAG,SAAS,CAAC;AAEhE;;;;GAIG;AACH,MAAM,MAAM,cAAc,GACtB,SAAS,GACT,aAAa,GACb,MAAM,GACN,SAAS,GACT,WAAW,GACX,UAAU,CAAC;AAEf;;;;;;;;GAQG;AACH,MAAM,WAAW,YAAY,CAAC,CAAC;IAC7B,4EAA4E;IAC5E,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,4DAA4D;IAC5D,QAAQ,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACjC,mDAAmD;IACnD,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,mDAAmD;IACnD,QAAQ,CAAC,IAAI,EAAE,YAAY,CAAC;IAC5B,wBAAwB;IACxB,QAAQ,CAAC,MAAM,EAAE,cAAc,CAAC;IAChC,+DAA+D;IAC/D,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,uCAAuC;IACvC,QAAQ,CAAC,QAAQ,EAAE,CAAC,CAAC;CACtB;AAED;;;;GAIG;AACH,MAAM,WAAW,YAAY,CAAC,CAAC;IAC7B,sDAAsD;IACtD,QAAQ,CAAC,IAAI,EAAE,aAAa,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC,CAAC;IAC9C,0FAA0F;IAC1F,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,sDAAsD;IACtD,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,sEAAsE;IACtE,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;CAC3B;AAED;;;;;GAKG;AACH,MAAM,WAAW,iBAAiB;IAChC,sCAAsC;IACtC,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IACxB,wCAAwC;IACxC,QAAQ,CAAC,KAAK,CAAC,EAAE,aAAa,CAAC,YAAY,CAAC,CAAC;IAC7C,uEAAuE;IACvE,QAAQ,CAAC,KAAK,CAAC,EAAE,OAAO,CAAC;IACzB,qEAAqE;IACrE,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IACzB,oEAAoE;IACpE,QAAQ,CAAC,QAAQ,CAAC,EAAE,OAAO,CAAC;IAC5B,kEAAkE;IAClE,QAAQ,CAAC,YAAY,CAAC,EAAE,OAAO,CAAC;CACjC;AAED;;;;;;;;;GASG;AACH,wBAAgB,cAAc,CAAC,CAAC,EAAE,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,YAAY,CAAC,CAAC,CAAC,CAS1E"}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Typed render contracts — tree (flattened hierarchy).
|
|
3
|
+
*
|
|
4
|
+
* Part of the Human Render Contract (Epic T10114, ADR-077). The flat-tree shape
|
|
5
|
+
* is the canonical wire format for hierarchical task / saga / epic listings —
|
|
6
|
+
* presenters re-materialise the parent/child structure from `parentId` + `depth`.
|
|
7
|
+
*
|
|
8
|
+
* @epic T10114
|
|
9
|
+
* @task T10138
|
|
10
|
+
*/
|
|
11
|
+
/**
|
|
12
|
+
* Runtime type guard for `TreeResponse<T>`.
|
|
13
|
+
*
|
|
14
|
+
* Verifies the envelope shape without inspecting node payloads — the generic
|
|
15
|
+
* `T` cannot be checked at runtime, so callers must layer per-node validation
|
|
16
|
+
* separately if they need stronger guarantees.
|
|
17
|
+
*
|
|
18
|
+
* @param value — candidate value to inspect.
|
|
19
|
+
* @returns `true` iff `value` matches the `TreeResponse<T>` envelope shape.
|
|
20
|
+
*/
|
|
21
|
+
export function isTreeResponse(value) {
|
|
22
|
+
if (typeof value !== 'object' || value === null)
|
|
23
|
+
return false;
|
|
24
|
+
const v = value;
|
|
25
|
+
return (Array.isArray(v.tree) &&
|
|
26
|
+
typeof v.root === 'string' &&
|
|
27
|
+
typeof v.totalNodes === 'number' &&
|
|
28
|
+
typeof v.maxDepth === 'number');
|
|
29
|
+
}
|
|
30
|
+
//# sourceMappingURL=tree.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tree.js","sourceRoot":"","sources":["../../src/render/tree.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAqFH;;;;;;;;;GASG;AACH,MAAM,UAAU,cAAc,CAAI,KAAc;IAC9C,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,KAAK,IAAI;QAAE,OAAO,KAAK,CAAC;IAC9D,MAAM,CAAC,GAAG,KAAgC,CAAC;IAC3C,OAAO,CACL,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,IAAI,CAAC;QACrB,OAAO,CAAC,CAAC,IAAI,KAAK,QAAQ;QAC1B,OAAO,CAAC,CAAC,UAAU,KAAK,QAAQ;QAChC,OAAO,CAAC,CAAC,QAAQ,KAAK,QAAQ,CAC/B,CAAC;AACJ,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cleocode/contracts",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "v2026.5.106",
|
|
4
4
|
"description": "Domain types, interfaces, and contracts for the CLEO ecosystem",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -138,7 +138,7 @@
|
|
|
138
138
|
"dependencies": {
|
|
139
139
|
"zod": "^4.3.6",
|
|
140
140
|
"zod-to-json-schema": "^3.25.2",
|
|
141
|
-
"@cleocode/lafs": "
|
|
141
|
+
"@cleocode/lafs": "v2026.5.106"
|
|
142
142
|
},
|
|
143
143
|
"scripts": {
|
|
144
144
|
"build": "tsc -b --force && node scripts/emit-schemas.mjs",
|
package/src/boundary.ts
CHANGED
|
@@ -318,9 +318,9 @@ export const BOUNDARY_REGISTRY: readonly BoundaryEntry[] = [
|
|
|
318
318
|
'Verified-dead code: 531 LOC Rust crate referenced ONLY by packages/core/src/llm/rust/__tests__/. Production transports (chat-completions/gemini/openai) import StreamingThinkScrubber from the TS think-scrubber.js path. CLEO_USE_RUST gate is never set in any workflow. Scheduled for deletion in T10205; see ADR-078 implementation tasks.',
|
|
319
319
|
},
|
|
320
320
|
{
|
|
321
|
-
module: 'conduit-core',
|
|
321
|
+
module: 'cleo-conduit-core',
|
|
322
322
|
intent: 'data-manifest',
|
|
323
|
-
rustCore: 'crates/conduit-core',
|
|
323
|
+
rustCore: 'crates/cleo-conduit-core',
|
|
324
324
|
canonicalHome: 'cleocode',
|
|
325
325
|
perfBudget: {
|
|
326
326
|
latency_p50_ms: 10,
|
|
@@ -331,7 +331,7 @@ export const BOUNDARY_REGISTRY: readonly BoundaryEntry[] = [
|
|
|
331
331
|
},
|
|
332
332
|
amendments: [],
|
|
333
333
|
rationale:
|
|
334
|
-
'Conduit serde-types crate (681 LOC) —
|
|
334
|
+
'Conduit serde-types crate (681 LOC) — renamed from conduit-core to cleo-conduit-core (T10185, saga T10180) because conduit-core is squatted on crates.io. Stable contract surface consumed by cant-core, signaldock-protocol, signaldock-core, integration-tests, AND external /mnt/projects/signaldock-core + /mnt/projects/signaldock/backend via git deps. Mirrors @cleocode/contracts/conduit.ts. Forced KEEP+publish=true: deleting breaks production, and signaldock-protocol publish requires it.',
|
|
335
335
|
},
|
|
336
336
|
{
|
|
337
337
|
module: 'integration-tests',
|
|
@@ -348,7 +348,7 @@ export const BOUNDARY_REGISTRY: readonly BoundaryEntry[] = [
|
|
|
348
348
|
},
|
|
349
349
|
amendments: [],
|
|
350
350
|
rationale:
|
|
351
|
-
'Internal Rust test harness (241 LOC) over lafs-core/conduit-core/cant-core/cant-router/signaldock-core. publish=false intentional — test code never ships.',
|
|
351
|
+
'Internal Rust test harness (241 LOC) over lafs-core/cleo-conduit-core/cant-core/cant-router/signaldock-core. publish=false intentional — test code never ships.',
|
|
352
352
|
},
|
|
353
353
|
{
|
|
354
354
|
module: 'lafs-core',
|
|
@@ -366,7 +366,7 @@ export const BOUNDARY_REGISTRY: readonly BoundaryEntry[] = [
|
|
|
366
366
|
},
|
|
367
367
|
amendments: [],
|
|
368
368
|
rationale:
|
|
369
|
-
'LAFS envelope serialization core (1,432 LOC) consumed by conduit-core, signaldock-protocol, lafs-napi, integration-tests. Mirrors @cleocode/lafs/src/envelope.ts. Flip publish=true to expose canonical envelope spec as a Rust library.',
|
|
369
|
+
'LAFS envelope serialization core (1,432 LOC) consumed by cleo-conduit-core, signaldock-protocol, lafs-napi, integration-tests. Mirrors @cleocode/lafs/src/envelope.ts. Flip publish=true to expose canonical envelope spec as a Rust library.',
|
|
370
370
|
},
|
|
371
371
|
{
|
|
372
372
|
module: 'lafs-napi',
|
package/src/errors.ts
CHANGED
|
@@ -519,3 +519,74 @@ export function isErrorResult(result: {
|
|
|
519
519
|
}): result is { success: false; error: string } {
|
|
520
520
|
return !result.success;
|
|
521
521
|
}
|
|
522
|
+
|
|
523
|
+
/**
|
|
524
|
+
* Structured details payload attached to {@link ChangesetYamlInvalidError}.
|
|
525
|
+
*
|
|
526
|
+
* Carries the offending file path, the 1-based line number where the YAML
|
|
527
|
+
* parser bailed (when determinable), and an optional snippet of the parsed
|
|
528
|
+
* region to surface in CLI output and CI logs.
|
|
529
|
+
*
|
|
530
|
+
* @task T10105
|
|
531
|
+
* @epic E-RELEASE-PLAN-CHANGELOG
|
|
532
|
+
*/
|
|
533
|
+
export interface ChangesetYamlInvalidDetails {
|
|
534
|
+
/** Absolute or process-relative path to the offending `.md` file. */
|
|
535
|
+
file: string;
|
|
536
|
+
/** 1-based line number reported by the YAML parser; `null` when unknown. */
|
|
537
|
+
line: number | null;
|
|
538
|
+
/** Optional short excerpt around the failure (≤ 120 chars). */
|
|
539
|
+
snippet?: string;
|
|
540
|
+
/** Underlying parser message, propagated verbatim for diagnostics. */
|
|
541
|
+
parserMessage: string;
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
/**
|
|
545
|
+
* Thrown when `cleo release plan` (or the lint script) encounters a `.changeset/*.md`
|
|
546
|
+
* file whose YAML frontmatter cannot be parsed.
|
|
547
|
+
*
|
|
548
|
+
* Replaces the pre-T10105 silent-skip behaviour where a single malformed
|
|
549
|
+
* file (e.g. an unquoted colon in `summary:`) caused the aggregator to
|
|
550
|
+
* silently drop ALL changeset entries for that release. The v2026.5.100
|
|
551
|
+
* ship lost the v5.100/v5.101/v5.103 CHANGELOG entries to exactly this
|
|
552
|
+
* vector — see Saga T10099 SG-RELEASE-AUDIT-V2.
|
|
553
|
+
*
|
|
554
|
+
* @remarks
|
|
555
|
+
* Carries `exitCode` aligned with {@link ExitCode.VALIDATION_ERROR} (6) so
|
|
556
|
+
* the CLI surfaces a deterministic non-zero exit when invoked from CI.
|
|
557
|
+
*
|
|
558
|
+
* @example
|
|
559
|
+
* ```typescript
|
|
560
|
+
* throw new ChangesetYamlInvalidError({
|
|
561
|
+
* file: '.changeset/bad.md',
|
|
562
|
+
* line: 4,
|
|
563
|
+
* parserMessage: 'Nested mappings are not allowed in compact mappings',
|
|
564
|
+
* });
|
|
565
|
+
* ```
|
|
566
|
+
*
|
|
567
|
+
* @task T10105
|
|
568
|
+
* @epic E-RELEASE-PLAN-CHANGELOG
|
|
569
|
+
* @saga T10099
|
|
570
|
+
*/
|
|
571
|
+
export class ChangesetYamlInvalidError extends Error {
|
|
572
|
+
/** Stable LAFS error code string for envelope emission. */
|
|
573
|
+
readonly code = 'E_CHANGESET_YAML_INVALID';
|
|
574
|
+
/** Numeric exit code aligned with {@link ExitCode.VALIDATION_ERROR} (6). */
|
|
575
|
+
readonly exitCode: ExitCode = ExitCode.VALIDATION_ERROR;
|
|
576
|
+
/** Structured payload describing the offending file + location. */
|
|
577
|
+
readonly details: ChangesetYamlInvalidDetails;
|
|
578
|
+
|
|
579
|
+
/**
|
|
580
|
+
* @param details - Structured failure context including file path and
|
|
581
|
+
* line number. The constructor builds a deterministic human-readable
|
|
582
|
+
* message of the form `<file>:<line> invalid YAML frontmatter: <msg>`.
|
|
583
|
+
*/
|
|
584
|
+
constructor(details: ChangesetYamlInvalidDetails) {
|
|
585
|
+
const linePart = details.line !== null ? `:${details.line}` : '';
|
|
586
|
+
super(
|
|
587
|
+
`E_CHANGESET_YAML_INVALID: ${details.file}${linePart} invalid YAML frontmatter: ${details.parserMessage}`,
|
|
588
|
+
);
|
|
589
|
+
this.name = 'ChangesetYamlInvalidError';
|
|
590
|
+
this.details = details;
|
|
591
|
+
}
|
|
592
|
+
}
|
package/src/exit-codes.ts
CHANGED
|
@@ -266,6 +266,19 @@ export const E_EVIDENCE_INSUFFICIENT = 'E_EVIDENCE_INSUFFICIENT' as const;
|
|
|
266
266
|
export const E_PLAN_NOT_FOUND = 'E_PLAN_NOT_FOUND' as const;
|
|
267
267
|
/** Generic release-plan validation failure (e.g. schema). Exit code 6. */
|
|
268
268
|
export const E_RELEASE_PLAN_INVALID = 'E_RELEASE_PLAN_INVALID' as const;
|
|
269
|
+
/**
|
|
270
|
+
* `cleo release plan` (or the lint script) encountered a `.changeset/*.md`
|
|
271
|
+
* file whose YAML frontmatter cannot be parsed. Exit code 6 (VALIDATION_ERROR).
|
|
272
|
+
*
|
|
273
|
+
* Replaces the pre-T10105 silent-skip that caused the v2026.5.100 ship to
|
|
274
|
+
* lose v5.100/v5.101/v5.103 CHANGELOG entries when a single changeset had
|
|
275
|
+
* an unquoted colon in `summary:`.
|
|
276
|
+
*
|
|
277
|
+
* @task T10105
|
|
278
|
+
* @epic E-RELEASE-PLAN-CHANGELOG
|
|
279
|
+
* @saga T10099
|
|
280
|
+
*/
|
|
281
|
+
export const E_CHANGESET_YAML_INVALID = 'E_CHANGESET_YAML_INVALID' as const;
|
|
269
282
|
/**
|
|
270
283
|
* Release row exists but is not in the status FSM state required by the
|
|
271
284
|
* caller (R-051 — `cleo release open` requires status='planned'). Exit code 6.
|
package/src/index.ts
CHANGED
|
@@ -349,9 +349,10 @@ export {
|
|
|
349
349
|
TASK_SEVERITIES,
|
|
350
350
|
TASK_SIZES,
|
|
351
351
|
} from './enums.js';
|
|
352
|
-
export type { SagaInvariantErrorCode } from './errors.js';
|
|
352
|
+
export type { ChangesetYamlInvalidDetails, SagaInvariantErrorCode } from './errors.js';
|
|
353
353
|
// === Error Utilities ===
|
|
354
354
|
export {
|
|
355
|
+
ChangesetYamlInvalidError,
|
|
355
356
|
ClassifierUnregisteredAgentError,
|
|
356
357
|
createErrorResult,
|
|
357
358
|
createSuccessResult,
|
|
@@ -397,6 +398,8 @@ export {
|
|
|
397
398
|
} from './evidence-record-schema.js';
|
|
398
399
|
// === Exit Codes ===
|
|
399
400
|
export {
|
|
401
|
+
// T10105 / Saga T10099 — fail-loud changeset parse
|
|
402
|
+
E_CHANGESET_YAML_INVALID,
|
|
400
403
|
// SPEC-T9345 release pipeline v2 error code names (T9525)
|
|
401
404
|
E_CHANNEL_MISMATCH,
|
|
402
405
|
E_DIRTY_TREE,
|
|
@@ -1480,6 +1483,15 @@ export {
|
|
|
1480
1483
|
TASK_KIND,
|
|
1481
1484
|
TaskKindSchema,
|
|
1482
1485
|
} from './release/plan.js';
|
|
1486
|
+
// === Release ship-e2e-smoke (T10103) ===
|
|
1487
|
+
export type {
|
|
1488
|
+
ShipE2eSmokeFinalState,
|
|
1489
|
+
ShipE2eSmokeParams,
|
|
1490
|
+
ShipE2eSmokeResult,
|
|
1491
|
+
ShipE2eSmokeStep,
|
|
1492
|
+
ShipE2eSmokeStepName,
|
|
1493
|
+
ShipE2eSmokeStepStatus,
|
|
1494
|
+
} from './release/ship-e2e-smoke.js';
|
|
1483
1495
|
// === Release Version Bump ===
|
|
1484
1496
|
export type {
|
|
1485
1497
|
BumpResult,
|
|
@@ -1490,6 +1502,8 @@ export type {
|
|
|
1490
1502
|
VersionBumpTarget,
|
|
1491
1503
|
VersionBumpTargetSource,
|
|
1492
1504
|
} from './release/version-bump.js';
|
|
1505
|
+
// === Human Render Contract (Epic T10114, ADR-077) ===
|
|
1506
|
+
export * from './render/index.js';
|
|
1493
1507
|
// === Result Types (Dashboard, Stats, Log, Context, Sequence, Analysis, Deps) ===
|
|
1494
1508
|
export type {
|
|
1495
1509
|
BottleneckTask,
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ship E2E Smoke contracts — types for the `cleo release ship-e2e-smoke`
|
|
3
|
+
* one-shot walker that exercises the full release lifecycle end-to-end.
|
|
4
|
+
*
|
|
5
|
+
* The smoke runs `plan` → `open` → wait-for-PR-merge → wait-for-tag →
|
|
6
|
+
* verify-npm-published. Each step records its status, duration, and any
|
|
7
|
+
* side-effect identifier (PR number, tag SHA, npm version) for downstream
|
|
8
|
+
* inspection. The aggregate envelope reports the final lifecycle state
|
|
9
|
+
* reached so failures can be resumed.
|
|
10
|
+
*
|
|
11
|
+
* Dry-run is the default; `--execute` flips the runner to perform real
|
|
12
|
+
* mutations. Both modes return the same envelope shape so consumers can
|
|
13
|
+
* diff a dry-run preview against a real run.
|
|
14
|
+
*
|
|
15
|
+
* @task T10103
|
|
16
|
+
* @epic E-CLEO-RELEASE-VERBS
|
|
17
|
+
* @saga T10099
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* The discrete steps the smoke walker executes, in order. The aggregate
|
|
22
|
+
* envelope records one entry per step with status + duration.
|
|
23
|
+
*/
|
|
24
|
+
export type ShipE2eSmokeStepName =
|
|
25
|
+
| 'plan'
|
|
26
|
+
| 'open'
|
|
27
|
+
| 'wait-for-pr'
|
|
28
|
+
| 'wait-for-tag'
|
|
29
|
+
| 'verify-npm-published';
|
|
30
|
+
|
|
31
|
+
/** Outcome of a single smoke step. */
|
|
32
|
+
export type ShipE2eSmokeStepStatus =
|
|
33
|
+
/** Step completed successfully (or, in dry-run, would have run). */
|
|
34
|
+
| 'ok'
|
|
35
|
+
/** Step was skipped — e.g. dry-run mode shortcuts post-plan. */
|
|
36
|
+
| 'skipped'
|
|
37
|
+
/** Step failed; the smoke envelope's success flag will be false. */
|
|
38
|
+
| 'failed';
|
|
39
|
+
|
|
40
|
+
/** Per-step result attached to the smoke envelope. */
|
|
41
|
+
export interface ShipE2eSmokeStep {
|
|
42
|
+
/** The step name. */
|
|
43
|
+
name: ShipE2eSmokeStepName;
|
|
44
|
+
/** Outcome. */
|
|
45
|
+
status: ShipE2eSmokeStepStatus;
|
|
46
|
+
/** Wall-clock duration in milliseconds. */
|
|
47
|
+
durationMs: number;
|
|
48
|
+
/**
|
|
49
|
+
* Optional human-readable detail string captured at step completion
|
|
50
|
+
* (e.g. "plan written to .cleo/release/v2026.5.104.plan.json",
|
|
51
|
+
* "PR #524 merged", "tag v2026.5.104 found", "npm @cleocode/cleo@…
|
|
52
|
+
* published").
|
|
53
|
+
*/
|
|
54
|
+
detail?: string;
|
|
55
|
+
/**
|
|
56
|
+
* Optional error message when `status === 'failed'`. Single-line so
|
|
57
|
+
* the envelope stays compact for CLI consumers.
|
|
58
|
+
*/
|
|
59
|
+
error?: string;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Final lifecycle state the smoke reached. Mirrors the `releases.status`
|
|
64
|
+
* column values plus a synthetic `npm-published` terminal state captured
|
|
65
|
+
* by the verify step.
|
|
66
|
+
*/
|
|
67
|
+
export type ShipE2eSmokeFinalState =
|
|
68
|
+
| 'not-started'
|
|
69
|
+
| 'planned'
|
|
70
|
+
| 'pr-opened'
|
|
71
|
+
| 'pr-merged'
|
|
72
|
+
| 'tag-pushed'
|
|
73
|
+
| 'npm-published';
|
|
74
|
+
|
|
75
|
+
/** Parameters accepted by the smoke walker. */
|
|
76
|
+
export interface ShipE2eSmokeParams {
|
|
77
|
+
/** Candidate release version (e.g. "v2026.6.0" or "2026.6.0"). */
|
|
78
|
+
version: string;
|
|
79
|
+
/** Epic task ID that scopes the release (forwarded to `release plan`). */
|
|
80
|
+
epicId: string;
|
|
81
|
+
/**
|
|
82
|
+
* When false (default), no side effects — every step reports what it
|
|
83
|
+
* WOULD do. When true, each step performs its real mutation.
|
|
84
|
+
*/
|
|
85
|
+
execute: boolean;
|
|
86
|
+
/**
|
|
87
|
+
* Total wall-clock budget across all polling waits, in milliseconds.
|
|
88
|
+
* Default 30 minutes when omitted. Per-step waits MUST respect this
|
|
89
|
+
* shared budget so a stuck PR doesn't hang the whole smoke.
|
|
90
|
+
*/
|
|
91
|
+
totalTimeoutMs?: number;
|
|
92
|
+
/**
|
|
93
|
+
* Poll interval for wait-* steps, in milliseconds. Default 5_000.
|
|
94
|
+
* Lowered in tests via DI to keep the suite fast.
|
|
95
|
+
*/
|
|
96
|
+
pollIntervalMs?: number;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** Top-level envelope returned by the smoke walker. */
|
|
100
|
+
export interface ShipE2eSmokeResult {
|
|
101
|
+
/** True when every step completed (or was intentionally skipped). */
|
|
102
|
+
success: boolean;
|
|
103
|
+
/** Echo of the version under smoke. */
|
|
104
|
+
version: string;
|
|
105
|
+
/** True when --execute was passed; false for dry-run preview. */
|
|
106
|
+
executed: boolean;
|
|
107
|
+
/** Ordered step log. */
|
|
108
|
+
steps: ShipE2eSmokeStep[];
|
|
109
|
+
/** Lifecycle state at smoke termination. */
|
|
110
|
+
finalState: ShipE2eSmokeFinalState;
|
|
111
|
+
/** Total wall-clock duration across all steps, in milliseconds. */
|
|
112
|
+
totalDurationMs: number;
|
|
113
|
+
}
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Compile-time + runtime narrowing tests for `RenderableEnvelope<T>` and the
|
|
3
|
+
* per-kind type guards.
|
|
4
|
+
*
|
|
5
|
+
* Each test constructs a valid envelope of a single `kind` and asserts that
|
|
6
|
+
* the matching guard narrows it correctly while every other guard returns
|
|
7
|
+
* `false`. This catches both schema drift and accidental short-circuits in
|
|
8
|
+
* the guards.
|
|
9
|
+
*
|
|
10
|
+
* @epic T10114
|
|
11
|
+
* @task T10141
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { describe, expect, it } from 'vitest';
|
|
15
|
+
import {
|
|
16
|
+
isGenericEnvelope,
|
|
17
|
+
isGroupedListEnvelope,
|
|
18
|
+
isListEnvelope,
|
|
19
|
+
isSectionEnvelope,
|
|
20
|
+
isSingleEnvelope,
|
|
21
|
+
isTableEnvelope,
|
|
22
|
+
isTreeEnvelope,
|
|
23
|
+
type RenderableEnvelope,
|
|
24
|
+
} from '../envelope.js';
|
|
25
|
+
import { isGroupedListResponse, isListResponse } from '../list.js';
|
|
26
|
+
import { isTableResponse } from '../table.js';
|
|
27
|
+
import { isTreeResponse } from '../tree.js';
|
|
28
|
+
|
|
29
|
+
interface Sample {
|
|
30
|
+
readonly x: number;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
describe('RenderableEnvelope<T> discriminated-union narrowing', () => {
|
|
34
|
+
it('narrows the tree variant and rejects all others', () => {
|
|
35
|
+
const env: RenderableEnvelope<Sample> = {
|
|
36
|
+
kind: 'tree',
|
|
37
|
+
data: { tree: [], root: 'T1', totalNodes: 0, maxDepth: 0 },
|
|
38
|
+
};
|
|
39
|
+
expect(isTreeEnvelope(env)).toBe(true);
|
|
40
|
+
expect(isTableEnvelope(env)).toBe(false);
|
|
41
|
+
expect(isListEnvelope(env)).toBe(false);
|
|
42
|
+
expect(isGroupedListEnvelope(env)).toBe(false);
|
|
43
|
+
expect(isSectionEnvelope(env)).toBe(false);
|
|
44
|
+
expect(isSingleEnvelope(env)).toBe(false);
|
|
45
|
+
expect(isGenericEnvelope(env)).toBe(false);
|
|
46
|
+
if (isTreeEnvelope(env)) {
|
|
47
|
+
expect(env.data.root).toBe('T1');
|
|
48
|
+
} else {
|
|
49
|
+
throw new Error('expected tree narrowing');
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('narrows the table variant and rejects all others', () => {
|
|
54
|
+
const env: RenderableEnvelope<Sample> = {
|
|
55
|
+
kind: 'table',
|
|
56
|
+
data: {
|
|
57
|
+
rows: [{ x: 1 }],
|
|
58
|
+
schema: { columns: [{ key: 'x', header: 'X' }] },
|
|
59
|
+
total: 1,
|
|
60
|
+
},
|
|
61
|
+
};
|
|
62
|
+
expect(isTableEnvelope(env)).toBe(true);
|
|
63
|
+
expect(isTreeEnvelope(env)).toBe(false);
|
|
64
|
+
if (isTableEnvelope(env)) {
|
|
65
|
+
expect(env.data.rows[0]?.x).toBe(1);
|
|
66
|
+
} else {
|
|
67
|
+
throw new Error('expected table narrowing');
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it('narrows the flat list variant', () => {
|
|
72
|
+
const env: RenderableEnvelope<Sample> = {
|
|
73
|
+
kind: 'list',
|
|
74
|
+
data: { items: [{ x: 7 }], total: 1, renderItem: 'kv' },
|
|
75
|
+
};
|
|
76
|
+
expect(isListEnvelope(env)).toBe(true);
|
|
77
|
+
expect(isGroupedListEnvelope(env)).toBe(false);
|
|
78
|
+
if (isListEnvelope(env)) {
|
|
79
|
+
expect(env.data.items[0]?.x).toBe(7);
|
|
80
|
+
expect(env.data.renderItem).toBe('kv');
|
|
81
|
+
} else {
|
|
82
|
+
throw new Error('expected list narrowing');
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it('narrows the grouped-list variant', () => {
|
|
87
|
+
const env: RenderableEnvelope<Sample> = {
|
|
88
|
+
kind: 'grouped-list',
|
|
89
|
+
data: {
|
|
90
|
+
groups: [{ key: 'a', label: 'A', items: [{ x: 2 }] }],
|
|
91
|
+
total: 1,
|
|
92
|
+
},
|
|
93
|
+
};
|
|
94
|
+
expect(isGroupedListEnvelope(env)).toBe(true);
|
|
95
|
+
expect(isListEnvelope(env)).toBe(false);
|
|
96
|
+
if (isGroupedListEnvelope(env)) {
|
|
97
|
+
expect(env.data.groups[0]?.items[0]?.x).toBe(2);
|
|
98
|
+
} else {
|
|
99
|
+
throw new Error('expected grouped-list narrowing');
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it('narrows the section variant', () => {
|
|
104
|
+
const env: RenderableEnvelope<Sample> = {
|
|
105
|
+
kind: 'section',
|
|
106
|
+
data: { header: 'Summary', icon: 'i', items: ['a', 'b'] },
|
|
107
|
+
};
|
|
108
|
+
expect(isSectionEnvelope(env)).toBe(true);
|
|
109
|
+
if (isSectionEnvelope(env)) {
|
|
110
|
+
expect(env.data.items).toEqual(['a', 'b']);
|
|
111
|
+
} else {
|
|
112
|
+
throw new Error('expected section narrowing');
|
|
113
|
+
}
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it('narrows the single variant', () => {
|
|
117
|
+
const env: RenderableEnvelope<Sample> = {
|
|
118
|
+
kind: 'single',
|
|
119
|
+
data: { x: 42 },
|
|
120
|
+
};
|
|
121
|
+
expect(isSingleEnvelope(env)).toBe(true);
|
|
122
|
+
if (isSingleEnvelope(env)) {
|
|
123
|
+
expect(env.data.x).toBe(42);
|
|
124
|
+
} else {
|
|
125
|
+
throw new Error('expected single narrowing');
|
|
126
|
+
}
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it('narrows the generic variant', () => {
|
|
130
|
+
const env: RenderableEnvelope<Sample> = {
|
|
131
|
+
kind: 'generic',
|
|
132
|
+
data: { foo: 'bar', count: 3 },
|
|
133
|
+
};
|
|
134
|
+
expect(isGenericEnvelope(env)).toBe(true);
|
|
135
|
+
if (isGenericEnvelope(env)) {
|
|
136
|
+
expect(env.data.foo).toBe('bar');
|
|
137
|
+
} else {
|
|
138
|
+
throw new Error('expected generic narrowing');
|
|
139
|
+
}
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it('exhaustively switches on kind without falling through', () => {
|
|
143
|
+
const envelopes: Array<RenderableEnvelope<Sample>> = [
|
|
144
|
+
{ kind: 'tree', data: { tree: [], root: 'r', totalNodes: 0, maxDepth: 0 } },
|
|
145
|
+
{
|
|
146
|
+
kind: 'table',
|
|
147
|
+
data: { rows: [], schema: { columns: [] }, total: 0 },
|
|
148
|
+
},
|
|
149
|
+
{ kind: 'list', data: { items: [], total: 0 } },
|
|
150
|
+
{ kind: 'grouped-list', data: { groups: [], total: 0 } },
|
|
151
|
+
{ kind: 'section', data: { header: 'h', items: [] } },
|
|
152
|
+
{ kind: 'single', data: { x: 0 } },
|
|
153
|
+
{ kind: 'generic', data: {} },
|
|
154
|
+
];
|
|
155
|
+
const seen = new Set<RenderableEnvelope<Sample>['kind']>();
|
|
156
|
+
for (const env of envelopes) {
|
|
157
|
+
switch (env.kind) {
|
|
158
|
+
case 'tree':
|
|
159
|
+
case 'table':
|
|
160
|
+
case 'list':
|
|
161
|
+
case 'grouped-list':
|
|
162
|
+
case 'section':
|
|
163
|
+
case 'single':
|
|
164
|
+
case 'generic':
|
|
165
|
+
seen.add(env.kind);
|
|
166
|
+
break;
|
|
167
|
+
default: {
|
|
168
|
+
const _exhaustive: never = env;
|
|
169
|
+
throw new Error(`unreachable: ${JSON.stringify(_exhaustive)}`);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
expect(seen.size).toBe(7);
|
|
174
|
+
});
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
describe('per-response type guards', () => {
|
|
178
|
+
it('accepts valid response shapes', () => {
|
|
179
|
+
expect(isTreeResponse({ tree: [], root: 'r', totalNodes: 0, maxDepth: 0 })).toBe(true);
|
|
180
|
+
expect(isTableResponse({ rows: [], schema: { columns: [] }, total: 0 })).toBe(true);
|
|
181
|
+
expect(isListResponse({ items: [], total: 0 })).toBe(true);
|
|
182
|
+
expect(isGroupedListResponse({ groups: [], total: 0 })).toBe(true);
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
it('rejects invalid response shapes', () => {
|
|
186
|
+
expect(isTreeResponse(null)).toBe(false);
|
|
187
|
+
expect(isTreeResponse({})).toBe(false);
|
|
188
|
+
expect(isTreeResponse({ tree: 'nope', root: 'r', totalNodes: 0, maxDepth: 0 })).toBe(false);
|
|
189
|
+
expect(isTableResponse({ rows: [], total: 0 })).toBe(false);
|
|
190
|
+
expect(isTableResponse({ rows: [], schema: 'nope', total: 0 })).toBe(false);
|
|
191
|
+
expect(isListResponse({ items: 'nope', total: 0 })).toBe(false);
|
|
192
|
+
expect(isGroupedListResponse({ groups: 'nope', total: 0 })).toBe(false);
|
|
193
|
+
});
|
|
194
|
+
});
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
|
2
|
+
import { ascii, BadgeIcon, KindIcon, pickIcon, RelationIcon, StatusIcon } from '../icon.js';
|
|
3
|
+
|
|
4
|
+
describe('icon enums', () => {
|
|
5
|
+
it('StatusIcon emoji values match ADR-077', () => {
|
|
6
|
+
expect(StatusIcon.PENDING).toBe('⏳');
|
|
7
|
+
expect(StatusIcon.ACTIVE).toBe('🚧');
|
|
8
|
+
expect(StatusIcon.DONE).toBe('✅');
|
|
9
|
+
expect(StatusIcon.BLOCKED).toBe('🚪');
|
|
10
|
+
expect(StatusIcon.ARCHIVED).toBe('🗄');
|
|
11
|
+
expect(StatusIcon.CANCELLED).toBe('✗');
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
it('KindIcon emoji values match ADR-077', () => {
|
|
15
|
+
expect(KindIcon.SAGA).toBe('🌲');
|
|
16
|
+
expect(KindIcon.EPIC).toBe('📋');
|
|
17
|
+
expect(KindIcon.TASK).toBe('•');
|
|
18
|
+
expect(KindIcon.SUBTASK).toBe('◦');
|
|
19
|
+
expect(KindIcon.RESEARCH).toBe('📖');
|
|
20
|
+
expect(KindIcon.BUG).toBe('🐛');
|
|
21
|
+
expect(KindIcon.RELEASE).toBe('🚀');
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it('BadgeIcon emoji values match ADR-077 (ORPHAN amended in T10137)', () => {
|
|
25
|
+
expect(BadgeIcon.EMPTY).toBe('🪦');
|
|
26
|
+
// ADR-077 §2 originally specified '🚪' for ORPHAN — identical to
|
|
27
|
+
// StatusIcon.BLOCKED. String enums share runtime values so ascii() cannot
|
|
28
|
+
// disambiguate. Amended to '👻' (abandoned/lonely). T10137 (B12) updates
|
|
29
|
+
// the ADR text to match.
|
|
30
|
+
expect(BadgeIcon.ORPHAN).toBe('👻');
|
|
31
|
+
expect(BadgeIcon.NESTED).toBe('🔁');
|
|
32
|
+
expect(BadgeIcon.CAUTION).toBe('⚠');
|
|
33
|
+
expect(BadgeIcon.NEW).toBe('★');
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it('RelationIcon emoji values match ADR-077', () => {
|
|
37
|
+
expect(RelationIcon.GROUPS).toBe('⊂');
|
|
38
|
+
expect(RelationIcon.PARENT).toBe('⤴');
|
|
39
|
+
expect(RelationIcon.DEPENDS).toBe('⇨');
|
|
40
|
+
expect(RelationIcon.BLOCKS).toBe('⊘');
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('no two enum members share the same runtime string value', () => {
|
|
44
|
+
const all = [
|
|
45
|
+
...Object.values(StatusIcon),
|
|
46
|
+
...Object.values(KindIcon),
|
|
47
|
+
...Object.values(BadgeIcon),
|
|
48
|
+
...Object.values(RelationIcon),
|
|
49
|
+
];
|
|
50
|
+
const unique = new Set(all);
|
|
51
|
+
expect(unique.size).toBe(all.length);
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
describe('ascii()', () => {
|
|
56
|
+
it('returns NO_COLOR fallback for every StatusIcon', () => {
|
|
57
|
+
expect(ascii(StatusIcon.PENDING)).toBe('[ ]');
|
|
58
|
+
expect(ascii(StatusIcon.ACTIVE)).toBe('[~]');
|
|
59
|
+
expect(ascii(StatusIcon.DONE)).toBe('[x]');
|
|
60
|
+
expect(ascii(StatusIcon.BLOCKED)).toBe('[!]');
|
|
61
|
+
expect(ascii(StatusIcon.ARCHIVED)).toBe('[#]');
|
|
62
|
+
expect(ascii(StatusIcon.CANCELLED)).toBe('[-]');
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it('returns NO_COLOR fallback for every KindIcon', () => {
|
|
66
|
+
expect(ascii(KindIcon.SAGA)).toBe('SG');
|
|
67
|
+
expect(ascii(KindIcon.EPIC)).toBe('E');
|
|
68
|
+
expect(ascii(KindIcon.TASK)).toBe('-');
|
|
69
|
+
expect(ascii(KindIcon.SUBTASK)).toBe('.');
|
|
70
|
+
expect(ascii(KindIcon.RESEARCH)).toBe('R');
|
|
71
|
+
expect(ascii(KindIcon.BUG)).toBe('B');
|
|
72
|
+
expect(ascii(KindIcon.RELEASE)).toBe('>');
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it('returns NO_COLOR fallback for every BadgeIcon', () => {
|
|
76
|
+
expect(ascii(BadgeIcon.EMPTY)).toBe('(empty)');
|
|
77
|
+
expect(ascii(BadgeIcon.ORPHAN)).toBe('(orphan)');
|
|
78
|
+
expect(ascii(BadgeIcon.NESTED)).toBe('(nested)');
|
|
79
|
+
expect(ascii(BadgeIcon.CAUTION)).toBe('(!)');
|
|
80
|
+
expect(ascii(BadgeIcon.NEW)).toBe('(new)');
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it('returns NO_COLOR fallback for every RelationIcon', () => {
|
|
84
|
+
expect(ascii(RelationIcon.GROUPS)).toBe('in');
|
|
85
|
+
expect(ascii(RelationIcon.PARENT)).toBe('^');
|
|
86
|
+
expect(ascii(RelationIcon.DEPENDS)).toBe('->');
|
|
87
|
+
expect(ascii(RelationIcon.BLOCKS)).toBe('!>');
|
|
88
|
+
});
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
describe('pickIcon()', () => {
|
|
92
|
+
const originalNoColor = process.env['NO_COLOR'];
|
|
93
|
+
const originalTerm = process.env['TERM'];
|
|
94
|
+
|
|
95
|
+
beforeEach(() => {
|
|
96
|
+
delete process.env['NO_COLOR'];
|
|
97
|
+
delete process.env['TERM'];
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
afterEach(() => {
|
|
101
|
+
if (originalNoColor === undefined) {
|
|
102
|
+
delete process.env['NO_COLOR'];
|
|
103
|
+
} else {
|
|
104
|
+
process.env['NO_COLOR'] = originalNoColor;
|
|
105
|
+
}
|
|
106
|
+
if (originalTerm === undefined) {
|
|
107
|
+
delete process.env['TERM'];
|
|
108
|
+
} else {
|
|
109
|
+
process.env['TERM'] = originalTerm;
|
|
110
|
+
}
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it('returns the emoji when noColor is explicitly false', () => {
|
|
114
|
+
expect(pickIcon(KindIcon.SAGA, { noColor: false })).toBe('🌲');
|
|
115
|
+
expect(pickIcon(StatusIcon.DONE, { noColor: false })).toBe('✅');
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it('returns the ASCII form when noColor is explicitly true', () => {
|
|
119
|
+
expect(pickIcon(KindIcon.SAGA, { noColor: true })).toBe('SG');
|
|
120
|
+
expect(pickIcon(StatusIcon.DONE, { noColor: true })).toBe('[x]');
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it('falls back to NO_COLOR=1 from process.env', () => {
|
|
124
|
+
process.env['NO_COLOR'] = '1';
|
|
125
|
+
expect(pickIcon(KindIcon.EPIC)).toBe('E');
|
|
126
|
+
expect(pickIcon(BadgeIcon.ORPHAN)).toBe('(orphan)');
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it('falls back to TERM=dumb from process.env', () => {
|
|
130
|
+
process.env['TERM'] = 'dumb';
|
|
131
|
+
expect(pickIcon(RelationIcon.GROUPS)).toBe('in');
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it('returns the emoji when no NO_COLOR signals are present', () => {
|
|
135
|
+
expect(pickIcon(KindIcon.TASK)).toBe('•');
|
|
136
|
+
expect(pickIcon(StatusIcon.ACTIVE)).toBe('🚧');
|
|
137
|
+
});
|
|
138
|
+
});
|