@@ -87,6 +87,33 @@ function changed(before: Map<string, string>, after: Map<string, string>): strin
8787 return [ ...names ] . filter ( ( n ) => before . get ( n ) !== after . get ( n ) ) . sort ( ) ;
8888}
8989
90+ /**
91+ * The message of the error `run` throws, for the cases where the MESSAGE is the
92+ * thing under test (#6751). `expect(...).toThrow()` cannot serve those: the
93+ * reader threw before the fix too — with a bare `TypeError` — so a throw-only
94+ * assertion is green on the defect it is supposed to pin.
95+ */
96+ function messageOf ( run : ( ) => unknown ) : string {
97+ try {
98+ run ( ) ;
99+ } catch ( error ) {
100+ return error instanceof Error ? error . message : String ( error ) ;
101+ }
102+ return expect . fail ( 'expected the shard reader to reject, but it returned a value' ) ;
103+ }
104+
105+ /** Rewrite one shard's parsed document, in the canonical byte shape. */
106+ function rewriteShard (
107+ root : string ,
108+ name : string ,
109+ mutate : ( doc : Record < string , unknown > ) => void ,
110+ ) : void {
111+ const file = path . join ( root , `${ name } .json` ) ;
112+ const doc = JSON . parse ( fs . readFileSync ( file , 'utf-8' ) ) ;
113+ mutate ( doc ) ;
114+ fs . writeFileSync ( file , JSON . stringify ( doc , null , 2 ) + '\n' ) ;
115+ }
116+
90117describe ( 'sharded artifacts — locality: a change in one category moves one file (#5837)' , ( ) => {
91118 it ( 'routes a key to the shard its category segment names, and only that one' , ( ) => {
92119 // The routing law itself. Everything below is a consequence of it, so it is
@@ -229,6 +256,79 @@ describe('sharded artifacts — the aggregate reads the whole directory (#5837)'
229256 expect ( ( ) => aggregateCategoryShards ( dir , 'keys' ) ) . toThrow ( / d e c l a r e s c a t e g o r y " d a t a " / ) ;
230257 } ) ;
231258
259+ /**
260+ * The fourth defect class of this reader (#6751). The other three name the
261+ * shard file and carry an issue anchor; a non-string entry used to reach
262+ * `categoryOfDefKey` — whose parameter is declared `string` — and die on
263+ * `key.indexOf is not a function`. All three call sites in `build-schemas.ts`
264+ * print `error.message` and nothing else, so that bare text WAS the whole
265+ * diagnostic: no file among 14 shards, no entry, no anchor.
266+ *
267+ * Note what these cases may not do: assert only that it throws. The unfixed
268+ * reader throws too, so `expect(...).toThrow()` — and even `toThrow(/./)` —
269+ * stays green on exactly the defect. The message is the contract here, so the
270+ * file, the entry and the anchor are each pinned by name.
271+ */
272+ it ( 'names the shard file, the entry index and the anchor when an entry is not a string' , ( ) => {
273+ writeShards ( dir , authorableSurfaceShardTexts ( KEYS ) ) ;
274+ rewriteShard ( dir , 'ui' , ( doc ) => {
275+ ( doc . keys as unknown [ ] ) [ 0 ] = 12345 ;
276+ } ) ;
277+
278+ const message = messageOf ( ( ) => aggregateCategoryShards ( dir , 'keys' ) ) ;
279+ expect ( message , 'names the shard file' ) . toContain ( 'ui.json' ) ;
280+ expect ( message , 'names the entry' ) . toContain ( 'keys[0]' ) ;
281+ expect ( message , 'carries the issue anchor' ) . toContain ( '#5837' ) ;
282+ expect ( message , 'says what was found instead' ) . toContain ( 'is a number, not a string' ) ;
283+ expect ( message , 'quotes the offending value' ) . toContain ( '12345' ) ;
284+ // The regression this exists for, stated as itself.
285+ expect ( message , 'never the bare JS error again' ) . not . toContain ( 'indexOf' ) ;
286+ } ) ;
287+
288+ it ( 'distinguishes null and object entries, which `typeof` alone reports as one' , ( ) => {
289+ // `typeof null === 'object'` is the trap the type label exists for: telling
290+ // an author "object" when they wrote `null` sends them looking for a brace.
291+ writeShards ( dir , authorableSurfaceShardTexts ( KEYS ) ) ;
292+ rewriteShard ( dir , 'ui' , ( doc ) => {
293+ ( doc . keys as unknown [ ] ) [ 1 ] = null ;
294+ } ) ;
295+ expect ( messageOf ( ( ) => aggregateCategoryShards ( dir , 'keys' ) ) ) . toContain (
296+ 'keys[1] is null, not a string' ,
297+ ) ;
298+
299+ writeShards ( dir , authorableSurfaceShardTexts ( KEYS ) ) ;
300+ rewriteShard ( dir , 'ui' , ( doc ) => {
301+ ( doc . keys as unknown [ ] ) [ 1 ] = { 'ui/View:name' : true } ;
302+ } ) ;
303+ expect ( messageOf ( ( ) => aggregateCategoryShards ( dir , 'keys' ) ) ) . toContain (
304+ 'keys[1] is an object, not a string' ,
305+ ) ;
306+ } ) ;
307+
308+ it ( 'speaks the same way for every sharded artifact, naming that artifact’s own field' , ( ) => {
309+ // `categoryOfDefKey` is shared by all three category-sharded ratchets
310+ // (authorable-surface/, json-schema.manifest/, authorable-defaults/), so the
311+ // dumb message appeared under three different prefixes. The field name is
312+ // part of the message for the same reason the file name is.
313+ //
314+ // The entry here is an ARRAY on purpose, and it is the worst of the class:
315+ // `['ui/View'].indexOf('/')` is a perfectly valid Array.prototype call that
316+ // answers -1, so this case never produced a TypeError at all — it fell
317+ // through to `cannot shard "ui/View": … has no category segment`, naming a
318+ // key that is not in the file and a cause that is not the defect. A wrong
319+ // diagnosis outranks a bare one, which is why the type is checked before
320+ // the routing rather than left to whatever `indexOf` happens to mean.
321+ writeShards ( dir , schemaManifestShardTexts ( [ 'ai/Agent' , 'ui/View' , 'ui/Dashboard' ] ) ) ;
322+ rewriteShard ( dir , 'ui' , ( doc ) => {
323+ ( doc . schemas as unknown [ ] ) [ 1 ] = [ 'ui/View' ] ;
324+ } ) ;
325+
326+ const message = messageOf ( ( ) => aggregateCategoryShards ( dir , 'schemas' ) ) ;
327+ expect ( message ) . toContain ( 'ui.json' ) ;
328+ expect ( message ) . toContain ( 'schemas[1] is an array, not a string' ) ;
329+ expect ( message ) . toContain ( '#5837' ) ;
330+ } ) ;
331+
232332 it ( 'refuses a stray file in a generator-owned directory' , ( ) => {
233333 writeShards ( dir , authorableSurfaceShardTexts ( KEYS ) ) ;
234334 fs . writeFileSync ( path . join ( dir , 'notes.txt' ) , 'scratch\n' ) ;
0 commit comments