11import { describe , expect , test } from 'bun:test' ;
2- import { mapInvokeError } from '../../lib/error-mapping' ;
2+ import { mapInvokeError , mapFailedReceipt } from '../../lib/error-mapping' ;
3+ import { CliError } from '../../lib/errors' ;
34
45describe ( 'mapInvokeError' , ( ) => {
56 test ( 'maps blocks.delete INVALID_INPUT errors to INVALID_ARGUMENT' , ( ) => {
@@ -14,3 +15,251 @@ describe('mapInvokeError', () => {
1415 expect ( mapped . details ) . toEqual ( { operationId : 'blocks.delete' , details : { field : 'target' } } ) ;
1516 } ) ;
1617} ) ;
18+
19+ // ---------------------------------------------------------------------------
20+ // T8: Plan-engine error code passthrough in CLI error mapping
21+ // ---------------------------------------------------------------------------
22+
23+ describe ( 'mapInvokeError: plan-engine error passthrough' , ( ) => {
24+ const operationId = 'mutations.apply' as any ;
25+
26+ test ( 'REVISION_MISMATCH preserves code and structured details' , ( ) => {
27+ const error = Object . assign ( new Error ( 'REVISION_MISMATCH — stale ref' ) , {
28+ code : 'REVISION_MISMATCH' ,
29+ details : {
30+ refRevision : '0' ,
31+ currentRevision : '2' ,
32+ refStability : 'ephemeral' ,
33+ remediation : 'Re-run query.match() to obtain a fresh ref.' ,
34+ } ,
35+ } ) ;
36+
37+ const result = mapInvokeError ( operationId , error ) ;
38+
39+ expect ( result ) . toBeInstanceOf ( CliError ) ;
40+ expect ( result . code ) . toBe ( 'REVISION_MISMATCH' ) ;
41+ expect ( result . details ) . toMatchObject ( {
42+ operationId,
43+ details : {
44+ refRevision : '0' ,
45+ currentRevision : '2' ,
46+ refStability : 'ephemeral' ,
47+ remediation : expect . any ( String ) ,
48+ } ,
49+ } ) ;
50+ } ) ;
51+
52+ test ( 'PLAN_CONFLICT_OVERLAP preserves code and matrix details' , ( ) => {
53+ const error = Object . assign ( new Error ( 'overlap' ) , {
54+ code : 'PLAN_CONFLICT_OVERLAP' ,
55+ details : {
56+ stepIdA : 'step-1' ,
57+ stepIdB : 'step-2' ,
58+ opKeyA : 'format.apply' ,
59+ opKeyB : 'text.rewrite' ,
60+ matrixVerdict : 'reject' ,
61+ matrixKey : 'format.apply::text.rewrite::same_target' ,
62+ } ,
63+ } ) ;
64+
65+ const result = mapInvokeError ( operationId , error ) ;
66+
67+ expect ( result . code ) . toBe ( 'PLAN_CONFLICT_OVERLAP' ) ;
68+ expect ( result . details ) . toMatchObject ( {
69+ details : {
70+ stepIdA : 'step-1' ,
71+ stepIdB : 'step-2' ,
72+ matrixVerdict : 'reject' ,
73+ } ,
74+ } ) ;
75+ } ) ;
76+
77+ test ( 'DOCUMENT_IDENTITY_CONFLICT preserves code and remediation' , ( ) => {
78+ const error = Object . assign ( new Error ( 'duplicate IDs' ) , {
79+ code : 'DOCUMENT_IDENTITY_CONFLICT' ,
80+ details : {
81+ duplicateBlockIds : [ 'p3' , 'p7' ] ,
82+ blockCount : 2 ,
83+ remediation : 'Re-import the document.' ,
84+ } ,
85+ } ) ;
86+
87+ const result = mapInvokeError ( operationId , error ) ;
88+
89+ expect ( result . code ) . toBe ( 'DOCUMENT_IDENTITY_CONFLICT' ) ;
90+ expect ( result . details ) . toMatchObject ( {
91+ details : {
92+ duplicateBlockIds : [ 'p3' , 'p7' ] ,
93+ remediation : expect . any ( String ) ,
94+ } ,
95+ } ) ;
96+ } ) ;
97+
98+ test ( 'REVISION_CHANGED_SINCE_COMPILE preserves code and details' , ( ) => {
99+ const error = Object . assign ( new Error ( 'drift' ) , {
100+ code : 'REVISION_CHANGED_SINCE_COMPILE' ,
101+ details : {
102+ compiledRevision : '3' ,
103+ currentRevision : '5' ,
104+ remediation : 'Re-compile the plan.' ,
105+ } ,
106+ } ) ;
107+
108+ const result = mapInvokeError ( operationId , error ) ;
109+
110+ expect ( result . code ) . toBe ( 'REVISION_CHANGED_SINCE_COMPILE' ) ;
111+ expect ( result . details ) . toMatchObject ( {
112+ details : {
113+ compiledRevision : '3' ,
114+ currentRevision : '5' ,
115+ } ,
116+ } ) ;
117+ } ) ;
118+
119+ test ( 'INVALID_INSERTION_CONTEXT preserves code and details' , ( ) => {
120+ const error = Object . assign ( new Error ( 'bad context' ) , {
121+ code : 'INVALID_INSERTION_CONTEXT' ,
122+ details : {
123+ stepIndex : 0 ,
124+ operation : 'create.heading' ,
125+ parentType : 'table_cell' ,
126+ } ,
127+ } ) ;
128+
129+ const result = mapInvokeError ( operationId , error ) ;
130+
131+ expect ( result . code ) . toBe ( 'INVALID_INSERTION_CONTEXT' ) ;
132+ expect ( result . details ) . toMatchObject ( {
133+ details : {
134+ stepIndex : 0 ,
135+ parentType : 'table_cell' ,
136+ } ,
137+ } ) ;
138+ } ) ;
139+
140+ test ( 'unknown error codes still fall through to COMMAND_FAILED' , ( ) => {
141+ const error = Object . assign ( new Error ( 'something weird' ) , {
142+ code : 'TOTALLY_UNKNOWN_CODE' ,
143+ details : { foo : 'bar' } ,
144+ } ) ;
145+
146+ const result = mapInvokeError ( operationId , error ) ;
147+
148+ expect ( result . code ) . toBe ( 'COMMAND_FAILED' ) ;
149+ } ) ;
150+
151+ test ( 'valid ref (no error) baseline — CliError passes through' , ( ) => {
152+ const error = new CliError ( 'COMMAND_FAILED' , 'already a CliError' ) ;
153+
154+ const result = mapInvokeError ( operationId , error ) ;
155+
156+ expect ( result ) . toBe ( error ) ;
157+ expect ( result . code ) . toBe ( 'COMMAND_FAILED' ) ;
158+ } ) ;
159+
160+ test ( 'large revision gap stale ref still includes all structured details' , ( ) => {
161+ const error = Object . assign ( new Error ( 'REVISION_MISMATCH' ) , {
162+ code : 'REVISION_MISMATCH' ,
163+ details : {
164+ refRevision : '0' ,
165+ currentRevision : '50' ,
166+ refStability : 'ephemeral' ,
167+ remediation : 'Re-run query.match()' ,
168+ } ,
169+ } ) ;
170+
171+ const result = mapInvokeError ( operationId , error ) ;
172+
173+ expect ( result . code ) . toBe ( 'REVISION_MISMATCH' ) ;
174+ expect ( result . details ) . toMatchObject ( {
175+ details : {
176+ refRevision : '0' ,
177+ currentRevision : '50' ,
178+ refStability : 'ephemeral' ,
179+ remediation : expect . any ( String ) ,
180+ } ,
181+ } ) ;
182+ } ) ;
183+ } ) ;
184+
185+ // ---------------------------------------------------------------------------
186+ // T8 extension: mapFailedReceipt — plan-engine code passthrough + envelope
187+ // ---------------------------------------------------------------------------
188+
189+ describe ( 'mapFailedReceipt: plan-engine code passthrough' , ( ) => {
190+ const operationId = 'insert' as any ;
191+
192+ test ( 'returns null for successful receipts' , ( ) => {
193+ expect ( mapFailedReceipt ( operationId , { success : true } ) ) . toBeNull ( ) ;
194+ } ) ;
195+
196+ test ( 'returns null for non-receipt values' , ( ) => {
197+ expect ( mapFailedReceipt ( operationId , 'not a receipt' ) ) . toBeNull ( ) ;
198+ expect ( mapFailedReceipt ( operationId , null ) ) . toBeNull ( ) ;
199+ expect ( mapFailedReceipt ( operationId , 42 ) ) . toBeNull ( ) ;
200+ } ) ;
201+
202+ test ( 'returns COMMAND_FAILED when failure has no code' , ( ) => {
203+ const result = mapFailedReceipt ( operationId , { success : false } ) ;
204+ expect ( result ) . toBeInstanceOf ( CliError ) ;
205+ expect ( result ! . code ) . toBe ( 'COMMAND_FAILED' ) ;
206+ } ) ;
207+
208+ test ( 'plan-engine code MATCH_NOT_FOUND passes through with structured details' , ( ) => {
209+ const receipt = {
210+ success : false ,
211+ failure : {
212+ code : 'MATCH_NOT_FOUND' ,
213+ message : 'No match found for selector' ,
214+ details : { selectorType : 'text' , selectorPattern : 'foo' , candidateCount : 0 } ,
215+ } ,
216+ } ;
217+
218+ const result = mapFailedReceipt ( operationId , receipt ) ;
219+ expect ( result ) . toBeInstanceOf ( CliError ) ;
220+ expect ( result ! . code ) . toBe ( 'MATCH_NOT_FOUND' ) ;
221+ expect ( result ! . details ) . toMatchObject ( {
222+ operationId,
223+ failure : { code : 'MATCH_NOT_FOUND' , details : { selectorType : 'text' } } ,
224+ } ) ;
225+ } ) ;
226+
227+ test ( 'plan-engine code PRECONDITION_FAILED passes through' , ( ) => {
228+ const receipt = {
229+ success : false ,
230+ failure : { code : 'PRECONDITION_FAILED' , message : 'Assert failed' } ,
231+ } ;
232+
233+ const result = mapFailedReceipt ( operationId , receipt ) ;
234+ expect ( result ! . code ) . toBe ( 'PRECONDITION_FAILED' ) ;
235+ } ) ;
236+
237+ test ( 'plan-engine code REVISION_MISMATCH passes through' , ( ) => {
238+ const receipt = {
239+ success : false ,
240+ failure : {
241+ code : 'REVISION_MISMATCH' ,
242+ message : 'stale ref' ,
243+ details : { refRevision : '0' , currentRevision : '3' } ,
244+ } ,
245+ } ;
246+
247+ const result = mapFailedReceipt ( operationId , receipt ) ;
248+ expect ( result ! . code ) . toBe ( 'REVISION_MISMATCH' ) ;
249+ expect ( result ! . details ) . toMatchObject ( {
250+ failure : { details : { refRevision : '0' , currentRevision : '3' } } ,
251+ } ) ;
252+ } ) ;
253+
254+ test ( 'non-plan-engine failure codes go through per-family normalization' , ( ) => {
255+ const receipt = {
256+ success : false ,
257+ failure : { code : 'NO_OP' , message : 'no change' } ,
258+ } ;
259+
260+ const result = mapFailedReceipt ( operationId , receipt ) ;
261+ // NO_OP is not a plan-engine passthrough code, so it normalizes
262+ expect ( result ) . toBeInstanceOf ( CliError ) ;
263+ expect ( result ! . code ) . not . toBe ( 'NO_OP' ) ;
264+ } ) ;
265+ } ) ;
0 commit comments