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.
- package/CHANGELOG.md +12 -0
- package/package.json +2 -1
- package/src/api/client.js +20 -2
- package/src/api/client.test.js +153 -0
- package/src/api/repos.js +23 -1
- package/src/auth/token_store.js +10 -1
- package/src/commands/match.js +185 -0
- package/src/commands/match.test.js +119 -0
- package/src/constants/error_codes.js +5 -0
- package/src/constants/next_step_actions.js +3 -1
- package/src/constants/next_step_by_error_code.js +13 -0
- package/src/constants.js +1 -0
- package/src/match/diff.js +41 -0
- package/src/match/funnel.js +179 -0
- package/src/match/funnel.test.js +157 -0
- package/src/match/scanner.js +129 -0
- package/src/match/scanner.test.js +93 -0
- package/src/utils/concurrency.js +28 -0
- package/src/utils/concurrency.test.js +53 -0
- package/src/utils/git_hash.js +29 -1
|
@@ -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
|
+
})
|
package/src/utils/git_hash.js
CHANGED
|
@@ -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
|
-
|
|
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 }
|