happyskills 1.13.0 → 1.14.0

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.
@@ -0,0 +1,53 @@
1
+ 'use strict'
2
+ // Spec 260629-03 §4.5 — bounded() over core-async throttle. The three load-
3
+ // bearing guarantees: never exceed the limit, complete every task in order, and
4
+ // a throwing task yields `{ error }` WITHOUT rejecting the whole batch (so one
5
+ // bad skill never aborts a match scan). Run with: node --test.
6
+
7
+ const { test } = require('node:test')
8
+ const assert = require('node:assert/strict')
9
+ const { bounded, DEFAULT_CONCURRENCY } = require('./concurrency')
10
+
11
+ test('never exceeds the limit, completes every task, preserves order', async () => {
12
+ let inflight = 0, peak = 0
13
+ const mk = (i) => async () => {
14
+ inflight++; peak = Math.max(peak, inflight)
15
+ await new Promise(r => setTimeout(r, 5))
16
+ inflight--
17
+ return i
18
+ }
19
+ const tasks = Array.from({ length: 20 }, (_, i) => mk(i))
20
+ const res = await bounded(tasks, 4)
21
+ assert.ok(peak <= 4, `peak concurrency ${peak} must not exceed the limit of 4`)
22
+ assert.deepEqual(res, Array.from({ length: 20 }, (_, i) => i), 'all tasks complete, order preserved')
23
+ })
24
+
25
+ test('a throwing task yields { error } without rejecting the batch', async () => {
26
+ const tasks = [
27
+ async () => 'ok-0',
28
+ async () => { throw new Error('boom-1') },
29
+ async () => 'ok-2',
30
+ ]
31
+ const res = await bounded(tasks, 2)
32
+ assert.equal(res[0], 'ok-0')
33
+ assert.ok(res[1] && res[1].error instanceof Error, 'failed task is partitioned as { error }')
34
+ assert.match(res[1].error.message, /boom-1/)
35
+ assert.equal(res[2], 'ok-2', 'sibling tasks still complete')
36
+ })
37
+
38
+ test('defaults to DEFAULT_CONCURRENCY and tolerates a bad limit', async () => {
39
+ assert.equal(DEFAULT_CONCURRENCY, 6)
40
+ let inflight = 0, peak = 0
41
+ const mk = () => async () => {
42
+ inflight++; peak = Math.max(peak, inflight)
43
+ await new Promise(r => setTimeout(r, 5))
44
+ inflight--
45
+ }
46
+ await bounded(Array.from({ length: 12 }, mk), 0) // 0 is invalid → falls back to the default
47
+ assert.ok(peak <= DEFAULT_CONCURRENCY, `bad limit must fall back to ${DEFAULT_CONCURRENCY}, peak was ${peak}`)
48
+ })
49
+
50
+ test('empty / non-array input resolves to an empty array', async () => {
51
+ assert.deepEqual(await bounded([], 4), [])
52
+ assert.deepEqual(await bounded(undefined, 4), [])
53
+ })
@@ -6,4 +6,32 @@ const hash_blob = (buf) => {
6
6
  return crypto.createHash('sha256').update(header).update(buf).digest('hex')
7
7
  }
8
8
 
9
- module.exports = { hash_blob }
9
+ // Git-tree hash over a flat entry list — an EXACT byte-for-byte mirror of the
10
+ // server's canonical `hash_tree` in api/app/utils/git_objects.js. The server
11
+ // builds the tree from the flat file list the CLI pushes, so the CLI never had
12
+ // a tree hash of its own; `happyskills match` needs one to reproduce a skill's
13
+ // catalog `tree_sha` locally and compare it (spec 260629-03 §4.6). Identity:
14
+ // equal tree_sha ⇒ byte-identical skill. Cross-package import is impossible
15
+ // (separate npm packages), so this is a deliberate mirror — if the server
16
+ // algorithm ever changes, change it here too (the e2e journey
17
+ // match-local-skills.spec.js proves the two stay byte-aligned end-to-end).
18
+ //
19
+ // Entries: [{ path, sha }] (sha = the file's blob hash). Sorted by
20
+ // path.localeCompare, serialized as `<mode> <path>\0<32-byte-binary-sha>`,
21
+ // wrapped in `tree <len>\0`, then sha256'd. mode defaults to '100644' (the
22
+ // server never sends a mode either).
23
+ const hash_tree = (entries) => {
24
+ const sorted = [...entries].sort((a, b) => a.path.localeCompare(b.path))
25
+ const parts = sorted.map(entry => {
26
+ const mode = entry.mode || '100644'
27
+ const prefix = Buffer.from(`${mode} ${entry.path}\0`)
28
+ const sha_bin = Buffer.from(entry.sha, 'hex')
29
+ return Buffer.concat([prefix, sha_bin])
30
+ })
31
+ const content = Buffer.concat(parts)
32
+ const header = Buffer.from(`tree ${content.length}\0`)
33
+ const full = Buffer.concat([header, content])
34
+ return crypto.createHash('sha256').update(full).digest('hex')
35
+ }
36
+
37
+ module.exports = { hash_blob, hash_tree }