@goliapkg/sentori-cli 0.6.0 → 1.1.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.
Files changed (47) hide show
  1. package/lib/index.js +269 -340
  2. package/lib/index.js.map +1 -1
  3. package/lib/issue.d.ts +16 -18
  4. package/lib/issue.d.ts.map +1 -1
  5. package/lib/issue.js +29 -32
  6. package/lib/issue.js.map +1 -1
  7. package/lib/lenient.d.ts +13 -0
  8. package/lib/lenient.d.ts.map +1 -0
  9. package/lib/lenient.js +25 -0
  10. package/lib/lenient.js.map +1 -0
  11. package/lib/mcp.d.ts +4 -2
  12. package/lib/mcp.d.ts.map +1 -1
  13. package/lib/mcp.js +43 -220
  14. package/lib/mcp.js.map +1 -1
  15. package/lib/native-artifacts.d.ts +0 -1
  16. package/lib/native-artifacts.d.ts.map +1 -1
  17. package/lib/native-artifacts.js +34 -41
  18. package/lib/native-artifacts.js.map +1 -1
  19. package/lib/probes.d.ts +10 -0
  20. package/lib/probes.d.ts.map +1 -0
  21. package/lib/probes.js +75 -0
  22. package/lib/probes.js.map +1 -0
  23. package/lib/react-native.d.ts.map +1 -1
  24. package/lib/react-native.js +17 -9
  25. package/lib/react-native.js.map +1 -1
  26. package/lib/upload.d.ts +21 -20
  27. package/lib/upload.d.ts.map +1 -1
  28. package/lib/upload.js +65 -57
  29. package/lib/upload.js.map +1 -1
  30. package/package.json +2 -2
  31. package/src/__tests__/lenient.test.ts +32 -0
  32. package/src/__tests__/mcp.test.ts +20 -112
  33. package/src/__tests__/native-artifacts.test.ts +43 -12
  34. package/src/__tests__/probes.test.ts +41 -0
  35. package/src/index.ts +291 -362
  36. package/src/issue.ts +47 -46
  37. package/src/lenient.ts +39 -0
  38. package/src/mcp.ts +46 -215
  39. package/src/native-artifacts.ts +42 -44
  40. package/src/probes.ts +75 -0
  41. package/src/react-native.ts +16 -9
  42. package/src/upload.ts +78 -68
  43. package/src/__tests__/issue.test.ts +0 -95
  44. package/src/__tests__/source-bundle-from-dir.test.ts +0 -85
  45. package/src/__tests__/source-bundle.test.ts +0 -105
  46. package/src/__tests__/upload.test.ts +0 -121
  47. package/src/source-bundle.ts +0 -234
@@ -3,7 +3,8 @@ import { existsSync, mkdtempSync, rmSync } from 'node:fs';
3
3
  import { createRequire } from 'node:module';
4
4
  import { tmpdir } from 'node:os';
5
5
  import { join } from 'node:path';
6
- import { uploadSourcemaps } from './upload.js';
6
+ import { uploadArtifact } from './upload.js';
7
+ import { basename } from 'node:path';
7
8
  /** Resolve `react-native/scripts/compose-source-maps.js` from the
8
9
  * current project's node_modules. Returns null if react-native isn't
9
10
  * installed or the version doesn't ship that script. */
@@ -52,14 +53,21 @@ export async function reactNativeUpload(opts) {
52
53
  const composed = composeSourceMaps(opts.metroMap, opts.hermesMap);
53
54
  try {
54
55
  const paths = [composed, ...(opts.bundle ? [opts.bundle] : [])];
55
- const r = await uploadSourcemaps({
56
- apiUrl: opts.apiUrl,
57
- dryRun: opts.dryRun,
58
- paths,
59
- release: opts.release,
60
- token: opts.token,
61
- });
62
- return { files: r.files, uploaded: r.uploaded };
56
+ if (opts.dryRun)
57
+ return { files: paths };
58
+ let uploaded = 0;
59
+ for (const p of paths) {
60
+ await uploadArtifact({
61
+ apiUrl: opts.apiUrl,
62
+ token: opts.token,
63
+ release: opts.release,
64
+ kind: 'sourcemap',
65
+ path: p,
66
+ name: basename(p),
67
+ });
68
+ uploaded += 1;
69
+ }
70
+ return { files: paths, uploaded };
63
71
  }
64
72
  finally {
65
73
  try {
@@ -1 +1 @@
1
- {"version":3,"file":"react-native.js","sourceRoot":"","sources":["../src/react-native.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"react-native.js","sourceRoot":"","sources":["../src/react-native.ts"],"names":[],"mappings":"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"}
package/lib/upload.d.ts CHANGED
@@ -1,24 +1,25 @@
1
- export type UploadOptions = {
2
- release: string;
3
- token: string;
4
- /** Sentori API base, e.g. https://sentori.golia.jp or your host. */
1
+ export type UploadOpts = {
5
2
  apiUrl: string;
6
- /** Files or directories. Directories are scanned one level deep. */
7
- paths: string[];
8
- dryRun?: boolean;
3
+ token: string;
4
+ release: string;
5
+ kind: 'dsym' | 'proguard' | 'sourcemap';
6
+ path: string;
7
+ /** Override the stored artifact name (defaults to the filename). */
8
+ name?: string;
9
9
  };
10
- export type UploadResult = {
11
- files: string[];
12
- uploaded?: number;
13
- artifacts?: {
14
- kind: string;
15
- name: string;
16
- }[];
10
+ /** Gzip a payload for the wire (pass-through if it already is gzip),
11
+ * renaming `foo` → `foo.gz` to match. Shared by every artifact
12
+ * upload path so dSYM/mapping and sourcemap cannot drift. */
13
+ export declare function gzipForWire(bytes: Buffer | Uint8Array, name: string): {
14
+ wire: Uint8Array;
15
+ wireName: string;
17
16
  };
18
- /** Resolve `paths` (files or dirs) to a deduped list of files to upload.
19
- * A directory contributes its top-level `.map` / `.js` / `.bundle` /
20
- * `.hbc` files; a file given explicitly is taken as-is (even if its
21
- * extension isn't in the list — the caller asked for it). */
22
- export declare function collectFiles(paths: string[]): Promise<string[]>;
23
- export declare function uploadSourcemaps(opts: UploadOptions): Promise<UploadResult>;
17
+ /** Exact-range copy into a standalone ArrayBuffer. `wire.buffer` is
18
+ * NOT safe here: Node pools small Buffers, so the backing buffer can
19
+ * be 16 KB of pool (offset view) a Blob built on it would ship
20
+ * garbage bytes around the payload. */
21
+ export declare function wireArrayBuffer(wire: Uint8Array): ArrayBuffer;
22
+ export declare function uploadArtifact(opts: UploadOpts): Promise<{
23
+ id: string;
24
+ }>;
24
25
  //# sourceMappingURL=upload.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"upload.d.ts","sourceRoot":"","sources":["../src/upload.ts"],"names":[],"mappings":"AASA,MAAM,MAAM,aAAa,GAAG;IAC1B,OAAO,EAAE,MAAM,CAAA;IACf,KAAK,EAAE,MAAM,CAAA;IACb,oEAAoE;IACpE,MAAM,EAAE,MAAM,CAAA;IACd,oEAAoE;IACpE,KAAK,EAAE,MAAM,EAAE,CAAA;IACf,MAAM,CAAC,EAAE,OAAO,CAAA;CACjB,CAAA;AAED,MAAM,MAAM,YAAY,GAAG;IACzB,KAAK,EAAE,MAAM,EAAE,CAAA;IACf,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,SAAS,CAAC,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,EAAE,CAAA;CAC7C,CAAA;AAED;;;8DAG8D;AAC9D,wBAAsB,YAAY,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,CAoBrE;AAED,wBAAsB,gBAAgB,CAAC,IAAI,EAAE,aAAa,GAAG,OAAO,CAAC,YAAY,CAAC,CAiCjF"}
1
+ {"version":3,"file":"upload.d.ts","sourceRoot":"","sources":["../src/upload.ts"],"names":[],"mappings":"AAiBA,MAAM,MAAM,UAAU,GAAG;IACvB,MAAM,EAAE,MAAM,CAAA;IACd,KAAK,EAAE,MAAM,CAAA;IACb,OAAO,EAAE,MAAM,CAAA;IACf,IAAI,EAAE,MAAM,GAAG,UAAU,GAAG,WAAW,CAAA;IACvC,IAAI,EAAE,MAAM,CAAA;IACZ,oEAAoE;IACpE,IAAI,CAAC,EAAE,MAAM,CAAA;CACd,CAAA;AAMD;;8DAE8D;AAC9D,wBAAgB,WAAW,CACzB,KAAK,EAAE,MAAM,GAAG,UAAU,EAC1B,IAAI,EAAE,MAAM,GACX;IAAE,IAAI,EAAE,UAAU,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAE,CAWxC;AAED;;;wCAGwC;AACxC,wBAAgB,eAAe,CAAC,IAAI,EAAE,UAAU,GAAG,WAAW,CAE7D;AAED,wBAAsB,cAAc,CAAC,IAAI,EAAE,UAAU,GAAG,OAAO,CAAC;IAAE,EAAE,EAAE,MAAM,CAAA;CAAE,CAAC,CAoC9E"}
package/lib/upload.js CHANGED
@@ -1,65 +1,73 @@
1
- import { readFile, readdir, stat } from 'node:fs/promises';
2
- import { basename, join } from 'node:path';
3
- // Files worth uploading: source maps, and the bundle they map (so the
4
- // dashboard can show the minified line too if a frame falls outside
5
- // the map). `.jsbundle` (iOS) and `.bundle` (Android) are RN's bundle
6
- // names; `.hbc` is the Hermes bytecode bundle.
7
- const UPLOADABLE = /\.(map|js|jsbundle|bundle|hbc)$/i;
8
- /** Resolve `paths` (files or dirs) to a deduped list of files to upload.
9
- * A directory contributes its top-level `.map` / `.js` / `.bundle` /
10
- * `.hbc` files; a file given explicitly is taken as-is (even if its
11
- * extension isn't in the list the caller asked for it). */
12
- export async function collectFiles(paths) {
13
- const out = [];
14
- for (const p of paths) {
15
- const s = await stat(p).catch(() => null);
16
- if (!s)
17
- throw new Error(`no such file or directory: ${p}`);
18
- if (s.isDirectory()) {
19
- for (const entry of await readdir(p)) {
20
- const full = join(p, entry);
21
- const es = await stat(full).catch(() => null);
22
- if (es?.isFile() && UPLOADABLE.test(entry))
23
- out.push(full);
24
- }
25
- }
26
- else {
27
- out.push(p);
28
- }
1
+ // Unified symbolication-artifact upload: sourcemap / dsym / proguard
2
+ // all land on `POST /v1/releases/{release}/artifacts` (multipart
3
+ // `kind` + `file`), authenticated with an api-scope token. A late
4
+ // upload triggers retro-symbolication server-side, which is what
5
+ // makes the lenient exit-0 contract honest nothing is lost
6
+ // forever.
7
+ //
8
+ // Uploads are gzipped on the wire (server 2.1.0 inflates
9
+ // transparently). Symbolication artifacts compress well a plain
10
+ // R8 mapping ~10:1, DWARF ~3:1 and the server's transport cap is
11
+ // smaller than its decompressed cap precisely so that the big ones
12
+ // (a real RN app's main dSYM runs hundreds of MB) fit as gzip.
13
+ import { readFileSync } from 'node:fs';
14
+ import { basename } from 'node:path';
15
+ import { gzipSync } from 'node:zlib';
16
+ /** Transport-side limit on the current server (256 MB). Used only to
17
+ * produce a useful error message the server is the authority. */
18
+ const WIRE_LIMIT = 256 * 1024 * 1024;
19
+ /** Gzip a payload for the wire (pass-through if it already is gzip),
20
+ * renaming `foo` `foo.gz` to match. Shared by every artifact
21
+ * upload path so dSYM/mapping and sourcemap cannot drift. */
22
+ export function gzipForWire(bytes, name) {
23
+ const alreadyGz = bytes.length >= 2 && bytes[0] === 0x1f && bytes[1] === 0x8b;
24
+ const wire = alreadyGz ? new Uint8Array(bytes) : new Uint8Array(gzipSync(bytes));
25
+ const wireName = alreadyGz || name.endsWith('.gz') ? name : `${name}.gz`;
26
+ if (wire.length > WIRE_LIMIT) {
27
+ throw new Error(`${name} is ${Math.round(wire.length / 1024 / 1024)} MB gzipped — ` +
28
+ `over the server's ${WIRE_LIMIT / 1024 / 1024} MB transport limit even compressed`);
29
29
  }
30
- const deduped = [...new Set(out)];
31
- if (deduped.length === 0) {
32
- throw new Error('no .map / .js / .bundle files found in the given path(s)');
33
- }
34
- return deduped;
30
+ return { wire, wireName };
31
+ }
32
+ /** Exact-range copy into a standalone ArrayBuffer. `wire.buffer` is
33
+ * NOT safe here: Node pools small Buffers, so the backing buffer can
34
+ * be 16 KB of pool (offset view) — a Blob built on it would ship
35
+ * garbage bytes around the payload. */
36
+ export function wireArrayBuffer(wire) {
37
+ return wire.buffer.slice(wire.byteOffset, wire.byteOffset + wire.byteLength);
35
38
  }
36
- export async function uploadSourcemaps(opts) {
37
- const files = await collectFiles(opts.paths);
38
- if (opts.dryRun)
39
- return { files };
39
+ export async function uploadArtifact(opts) {
40
+ const bytes = readFileSync(opts.path);
41
+ if (bytes.length === 0)
42
+ throw new Error(`empty file: ${opts.path}`);
43
+ // Already-compressed input (foo.map.gz) is passed through; the
44
+ // server strips the .gz suffix from the stored name after inflating.
45
+ const { wire, wireName } = gzipForWire(bytes, opts.name ?? basename(opts.path));
40
46
  const form = new FormData();
41
- for (const f of files) {
42
- const buf = await readFile(f);
43
- form.append('file', new Blob([buf]), basename(f));
47
+ form.append('kind', opts.kind);
48
+ form.append('file', new Blob([wireArrayBuffer(wire)]), wireName);
49
+ const url = `${opts.apiUrl.replace(/\/+$/, '')}/v1/releases/${encodeURIComponent(opts.release)}/artifacts`;
50
+ let resp;
51
+ try {
52
+ resp = await fetch(url, {
53
+ method: 'POST',
54
+ headers: { Authorization: `Bearer ${opts.token}` },
55
+ body: form,
56
+ });
57
+ }
58
+ catch (e) {
59
+ // A proxy that hits its body-size cap tends to reset the
60
+ // connection instead of answering, which surfaces here as a bare
61
+ // network error. Say so — "fetch failed" cost one team a round of
62
+ // debugging before they found the 413 underneath.
63
+ throw new Error(`network error (${e instanceof Error ? e.message : String(e)}) — ` +
64
+ `if the file is large this can be a proxy body-size cap rejecting ` +
65
+ `the upload (${Math.round(wire.length / 1024 / 1024)} MB on the wire)`);
44
66
  }
45
- const base = opts.apiUrl.replace(/\/+$/, '');
46
- const url = `${base}/admin/api/releases/${encodeURIComponent(opts.release)}/sourcemaps`;
47
- const resp = await fetch(url, {
48
- body: form,
49
- headers: { Authorization: `Bearer ${opts.token}` },
50
- method: 'POST',
51
- });
52
67
  if (!resp.ok) {
53
- let detail = '';
54
- try {
55
- detail = await resp.text();
56
- }
57
- catch {
58
- // ignore
59
- }
60
- throw new Error(`${resp.status} ${resp.statusText}${detail ? ` — ${detail.slice(0, 300)}` : ''}`);
68
+ const detail = await resp.text().catch(() => '');
69
+ throw new Error(`${resp.status} ${resp.statusText}${detail ? ` — ${detail.slice(0, 200)}` : ''}`);
61
70
  }
62
- const body = (await resp.json().catch(() => ({})));
63
- return { artifacts: body.artifacts, files, uploaded: body.uploaded ?? files.length };
71
+ return (await resp.json());
64
72
  }
65
73
  //# sourceMappingURL=upload.js.map
package/lib/upload.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"upload.js","sourceRoot":"","sources":["../src/upload.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,kBAAkB,CAAA;AAC1D,OAAO,EAAE,QAAQ,EAAE,IAAI,EAAE,MAAM,WAAW,CAAA;AAE1C,sEAAsE;AACtE,oEAAoE;AACpE,sEAAsE;AACtE,+CAA+C;AAC/C,MAAM,UAAU,GAAG,kCAAkC,CAAA;AAkBrD;;;8DAG8D;AAC9D,MAAM,CAAC,KAAK,UAAU,YAAY,CAAC,KAAe;IAChD,MAAM,GAAG,GAAa,EAAE,CAAA;IACxB,KAAK,MAAM,CAAC,IAAI,KAAK,EAAE,CAAC;QACtB,MAAM,CAAC,GAAG,MAAM,IAAI,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,IAAI,CAAC,CAAA;QACzC,IAAI,CAAC,CAAC;YAAE,MAAM,IAAI,KAAK,CAAC,8BAA8B,CAAC,EAAE,CAAC,CAAA;QAC1D,IAAI,CAAC,CAAC,WAAW,EAAE,EAAE,CAAC;YACpB,KAAK,MAAM,KAAK,IAAI,MAAM,OAAO,CAAC,CAAC,CAAC,EAAE,CAAC;gBACrC,MAAM,IAAI,GAAG,IAAI,CAAC,CAAC,EAAE,KAAK,CAAC,CAAA;gBAC3B,MAAM,EAAE,GAAG,MAAM,IAAI,CAAC,IAAI,CAAC,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,IAAI,CAAC,CAAA;gBAC7C,IAAI,EAAE,EAAE,MAAM,EAAE,IAAI,UAAU,CAAC,IAAI,CAAC,KAAK,CAAC;oBAAE,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;YAC5D,CAAC;QACH,CAAC;aAAM,CAAC;YACN,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC,CAAA;QACb,CAAC;IACH,CAAC;IACD,MAAM,OAAO,GAAG,CAAC,GAAG,IAAI,GAAG,CAAC,GAAG,CAAC,CAAC,CAAA;IACjC,IAAI,OAAO,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACzB,MAAM,IAAI,KAAK,CAAC,0DAA0D,CAAC,CAAA;IAC7E,CAAC;IACD,OAAO,OAAO,CAAA;AAChB,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,gBAAgB,CAAC,IAAmB;IACxD,MAAM,KAAK,GAAG,MAAM,YAAY,CAAC,IAAI,CAAC,KAAK,CAAC,CAAA;IAC5C,IAAI,IAAI,CAAC,MAAM;QAAE,OAAO,EAAE,KAAK,EAAE,CAAA;IAEjC,MAAM,IAAI,GAAG,IAAI,QAAQ,EAAE,CAAA;IAC3B,KAAK,MAAM,CAAC,IAAI,KAAK,EAAE,CAAC;QACtB,MAAM,GAAG,GAAG,MAAM,QAAQ,CAAC,CAAC,CAAC,CAAA;QAC7B,IAAI,CAAC,MAAM,CAAC,MAAM,EAAE,IAAI,IAAI,CAAC,CAAC,GAAG,CAAC,CAAC,EAAE,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAA;IACnD,CAAC;IAED,MAAM,IAAI,GAAG,IAAI,CAAC,MAAM,CAAC,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC,CAAA;IAC5C,MAAM,GAAG,GAAG,GAAG,IAAI,uBAAuB,kBAAkB,CAAC,IAAI,CAAC,OAAO,CAAC,aAAa,CAAA;IACvF,MAAM,IAAI,GAAG,MAAM,KAAK,CAAC,GAAG,EAAE;QAC5B,IAAI,EAAE,IAAI;QACV,OAAO,EAAE,EAAE,aAAa,EAAE,UAAU,IAAI,CAAC,KAAK,EAAE,EAAE;QAClD,MAAM,EAAE,MAAM;KACf,CAAC,CAAA;IACF,IAAI,CAAC,IAAI,CAAC,EAAE,EAAE,CAAC;QACb,IAAI,MAAM,GAAG,EAAE,CAAA;QACf,IAAI,CAAC;YACH,MAAM,GAAG,MAAM,IAAI,CAAC,IAAI,EAAE,CAAA;QAC5B,CAAC;QAAC,MAAM,CAAC;YACP,SAAS;QACX,CAAC;QACD,MAAM,IAAI,KAAK,CACb,GAAG,IAAI,CAAC,MAAM,IAAI,IAAI,CAAC,UAAU,GAAG,MAAM,CAAC,CAAC,CAAC,MAAM,MAAM,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,EAAE,CACjF,CAAA;IACH,CAAC;IACD,MAAM,IAAI,GAAG,CAAC,MAAM,IAAI,CAAC,IAAI,EAAE,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,CAAC,EAAE,CAAC,CAAC,CAGhD,CAAA;IACD,OAAO,EAAE,SAAS,EAAE,IAAI,CAAC,SAAS,EAAE,KAAK,EAAE,QAAQ,EAAE,IAAI,CAAC,QAAQ,IAAI,KAAK,CAAC,MAAM,EAAE,CAAA;AACtF,CAAC"}
1
+ {"version":3,"file":"upload.js","sourceRoot":"","sources":["../src/upload.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@goliapkg/sentori-cli",
3
- "version": "0.6.0",
3
+ "version": "1.1.0",
4
4
  "description": "Sentori CLI — upload source maps so dashboard stack traces resolve to your original source.",
5
5
  "license": "Apache-2.0 OR MIT",
6
6
  "author": "GOLIA K.K. <takagi@golia.jp> (https://golia.jp)",
@@ -42,7 +42,7 @@
42
42
  "devDependencies": {
43
43
  "@types/bun": "latest",
44
44
  "@types/node": "*",
45
- "typescript": "^6"
45
+ "typescript": "^7"
46
46
  },
47
47
  "publishConfig": {
48
48
  "access": "public"
@@ -0,0 +1,32 @@
1
+ // Build-time iron rule: upload failure exits 0 with a friendly,
2
+ // actionable note; --strict opts into a real non-zero.
3
+
4
+ import { describe, expect, test } from 'bun:test'
5
+
6
+ import { isStrict, lenientFail, stripStrict } from '../lenient.js'
7
+
8
+ describe('lenient upload contract', () => {
9
+ test('default: failure exits 0', () => {
10
+ const code = lenientFail(false, {
11
+ failure: 'network down',
12
+ impact: 'stacks stay minified',
13
+ retry: 'sentori-cli upload sourcemap ...',
14
+ })
15
+ expect(code).toBe(0)
16
+ })
17
+
18
+ test('--strict: failure exits 1', () => {
19
+ const code = lenientFail(true, {
20
+ failure: 'network down',
21
+ impact: 'stacks stay minified',
22
+ retry: 'retry-cmd',
23
+ })
24
+ expect(code).toBe(1)
25
+ })
26
+
27
+ test('strict flag parses and strips', () => {
28
+ expect(isStrict(['--release', 'r', '--strict'])).toBe(true)
29
+ expect(isStrict(['--release', 'r'])).toBe(false)
30
+ expect(stripStrict(['--strict', 'a'])).toEqual(['a'])
31
+ })
32
+ })
@@ -1,124 +1,32 @@
1
- import { afterEach, beforeEach, describe, expect, test } from 'bun:test'
1
+ // The MCP tool surface mirrors the /api closed loop 1:1.
2
2
 
3
- import { buildTools } from '../mcp.js'
4
-
5
- const ADMIN = {
6
- apiUrl: 'https://api.example.com/',
7
- projectId: 'proj-uuid',
8
- token: 'sk_test',
9
- }
10
-
11
- const origFetch = globalThis.fetch
12
- let calls: { body: string | null; headers: Headers; method: string; url: string }[]
3
+ import { describe, expect, test } from 'bun:test'
13
4
 
14
- beforeEach(() => {
15
- calls = []
16
- })
17
- afterEach(() => {
18
- globalThis.fetch = origFetch
19
- })
5
+ import { buildTools } from '../mcp.js'
20
6
 
21
- function mockFetch(status = 200, body: unknown = {}): void {
22
- globalThis.fetch = (async (url: Request | string | URL, init?: RequestInit) => {
23
- const b = init?.body
24
- calls.push({
25
- body: typeof b === 'string' ? b : null,
26
- headers: new Headers(init?.headers),
27
- method: init?.method ?? 'GET',
28
- url: String(url),
29
- })
30
- return new Response(status === 204 ? '' : JSON.stringify(body), { status })
31
- }) as typeof fetch
32
- }
7
+ describe('mcp tools', () => {
8
+ const tools = buildTools()
33
9
 
34
- describe('buildTools', () => {
35
- test('returns the expected set of tools with required input schema fields', () => {
36
- const tools = buildTools()
37
- const names = new Set(tools.map((t) => t.name))
38
- for (const expected of [
10
+ test('exactly the four closed-loop tools exist', () => {
11
+ expect(tools.map((t) => t.name).sort()).toEqual([
12
+ 'sentori_issue_bundle',
39
13
  'sentori_issue_list',
40
- 'sentori_issue_get',
41
- 'sentori_issue_comment',
42
- 'sentori_issue_transition',
43
- 'sentori_issue_assign',
44
- 'sentori_issue_set_priority',
45
- 'sentori_issue_set_labels',
46
- 'sentori_issue_watch',
47
- ]) {
48
- expect(names.has(expected)).toBe(true)
49
- }
14
+ 'sentori_issue_note',
15
+ 'sentori_issue_resolve',
16
+ ])
17
+ })
18
+
19
+ test('every tool has a description and an object schema', () => {
50
20
  for (const t of tools) {
51
- // Every tool advertises a description + an input schema.
52
- expect(typeof t.description).toBe('string')
53
- expect(t.inputSchema).toBeDefined()
21
+ expect(t.description.length).toBeGreaterThan(20)
54
22
  expect((t.inputSchema as { type?: string }).type).toBe('object')
55
23
  }
56
24
  })
57
- })
58
25
 
59
- describe('tool handlers proxy to admin API', () => {
60
- test('sentori_issue_list calls GET .../issues with filters', async () => {
61
- const tools = buildTools()
62
- const list = tools.find((t) => t.name === 'sentori_issue_list')!
63
- mockFetch(200, [])
64
- await list.handler(
65
- { limit: 25, priority: 'p0', projectId: 'proj-uuid', status: 'active' },
66
- ADMIN,
67
- )
68
- expect(calls.length).toBe(1)
69
- expect(calls[0]!.url).toContain('/admin/api/projects/proj-uuid/issues?')
70
- expect(calls[0]!.url).toContain('status=active')
71
- expect(calls[0]!.url).toContain('priority=p0')
72
- expect(calls[0]!.url).toContain('limit=25')
73
- expect(calls[0]!.method).toBe('GET')
74
- expect(calls[0]!.headers.get('authorization')).toBe('Bearer sk_test')
75
- })
76
-
77
- test('sentori_issue_get fetches issue + activity in parallel', async () => {
78
- const tools = buildTools()
79
- const get = tools.find((t) => t.name === 'sentori_issue_get')!
80
- mockFetch(200, { stub: true })
81
- await get.handler({ issueId: 'iss-uuid', projectId: 'proj-uuid' }, ADMIN)
82
- expect(calls.length).toBe(2)
83
- const paths = calls.map((c) => c.url).sort()
84
- expect(paths[0]).toContain('/admin/api/projects/proj-uuid/issues/iss-uuid')
85
- expect(paths[1]).toContain('/admin/api/projects/proj-uuid/issues/iss-uuid/activity')
86
- })
87
-
88
- test('sentori_issue_transition PATCHes status', async () => {
89
- const tools = buildTools()
90
- const t = tools.find((x) => x.name === 'sentori_issue_transition')!
91
- mockFetch(200, { id: 'x' })
92
- await t.handler({ issueId: 'iss', projectId: 'proj', status: 'resolved' }, ADMIN)
93
- expect(calls[0]!.method).toBe('PATCH')
94
- expect(JSON.parse(calls[0]!.body!).status).toBe('resolved')
95
- })
96
-
97
- test('sentori_issue_set_labels rejects non-string array entries', async () => {
98
- const tools = buildTools()
99
- const t = tools.find((x) => x.name === 'sentori_issue_set_labels')!
100
- await expect(
101
- t.handler({ issueId: 'iss', labels: ['ok', 42], projectId: 'proj' }, ADMIN),
102
- ).rejects.toThrow(/each label/)
103
- })
104
-
105
- test('sentori_issue_watch=true → PUT, watch=false → DELETE', async () => {
106
- const tools = buildTools()
107
- const t = tools.find((x) => x.name === 'sentori_issue_watch')!
108
- mockFetch(204)
109
- await t.handler({ issueId: 'iss', projectId: 'proj', watch: true }, ADMIN)
110
- expect(calls[0]!.method).toBe('PUT')
111
- calls.length = 0
112
- await t.handler({ issueId: 'iss', projectId: 'proj', watch: false }, ADMIN)
113
- expect(calls[0]!.method).toBe('DELETE')
114
- })
115
-
116
- test('handler surfaces a non-2xx as an Error', async () => {
117
- const tools = buildTools()
118
- const t = tools.find((x) => x.name === 'sentori_issue_comment')!
119
- mockFetch(500, { error: 'boom' })
120
- await expect(
121
- t.handler({ body: 'hello', issueId: 'iss', projectId: 'proj' }, ADMIN),
122
- ).rejects.toThrow(/500/)
26
+ test('bundle/note/resolve require issueId', () => {
27
+ for (const name of ['sentori_issue_bundle', 'sentori_issue_note', 'sentori_issue_resolve']) {
28
+ const t = tools.find((x) => x.name === name)!
29
+ expect((t.inputSchema as { required?: string[] }).required).toContain('issueId')
30
+ }
123
31
  })
124
32
  })
@@ -13,8 +13,7 @@ import {
13
13
 
14
14
  const ADMIN = {
15
15
  apiUrl: 'https://api.example.com/',
16
- projectId: 'proj-1',
17
- token: 'sk_test',
16
+ token: 'sk_test',
18
17
  }
19
18
 
20
19
  let dir = ''
@@ -51,19 +50,17 @@ describe('uploadMapping', () => {
51
50
  expect(calls).toHaveLength(1)
52
51
  expect(calls[0]?.method).toBe('POST')
53
52
  expect(calls[0]?.url).toBe(
54
- 'https://api.example.com/admin/api/projects/proj-1/mappings?release=app%401.0.0%2B1',
53
+ 'https://api.example.com/v1/releases/app%401.0.0%2B1/artifacts',
55
54
  )
56
55
  expect(calls[0]?.headers.get('authorization')).toBe('Bearer sk_test')
57
- expect(calls[0]?.headers.get('content-type')).toBe('application/octet-stream')
58
- expect(calls[0]?.body?.toString()).toContain('pg_map_id')
59
56
  })
60
57
 
61
58
  test('sets x-sentori-debug-id when given', async () => {
62
59
  mockFetch()
63
60
  const path = join(dir, 'mapping.txt')
64
61
  await writeFile(path, 'x')
65
- await uploadMapping({ ...ADMIN, debugId: '1234-uuid', path })
66
- expect(calls[0]?.headers.get('x-sentori-debug-id')).toBe('1234-uuid')
62
+ await uploadMapping({ ...ADMIN, debugId: '1234-uuid', path, release: 'r' })
63
+ expect(calls[0]?.url).toContain('/v1/releases/r/artifacts')
67
64
  })
68
65
 
69
66
  test('throws on empty file', async () => {
@@ -96,11 +93,7 @@ describe('uploadDsym (explicit single-slice)', () => {
96
93
  })
97
94
  expect(calls).toHaveLength(1)
98
95
  expect(calls[0]?.method).toBe('POST')
99
- expect(calls[0]?.url).toContain('/admin/api/projects/proj-1/dsyms')
100
- expect(calls[0]?.url).toContain('release=app%401.0.0%2B1')
101
- expect(calls[0]?.url).toContain('objectName=Foo')
102
- expect(calls[0]?.headers.get('x-sentori-debug-id')).toBe('1234ABCD-1234-1234-1234-1234567890AB')
103
- expect(calls[0]?.headers.get('x-sentori-arch')).toBe('arm64')
96
+ expect(calls[0]?.url).toContain('/v1/releases/app%401.0.0%2B1/artifacts')
104
97
  expect(r.slices).toEqual([{ arch: 'arm64', debugId: '1234ABCD-1234-1234-1234-1234567890AB' }])
105
98
  })
106
99
  })
@@ -130,3 +123,41 @@ describe('helpers', () => {
130
123
  expect(slices).toEqual([])
131
124
  })
132
125
  })
126
+
127
+ describe('gzip on the wire', () => {
128
+ test('mapping upload body is gzip and filename gains .gz', async () => {
129
+ const captured: { name: string; bytes: Uint8Array }[] = []
130
+ globalThis.fetch = (async (_url: Request | string | URL, init?: RequestInit) => {
131
+ const form = init?.body as FormData
132
+ const file = form.get('file') as File
133
+ captured.push({
134
+ bytes: new Uint8Array(await file.arrayBuffer()),
135
+ name: file.name,
136
+ })
137
+ return new Response('{}', { status: 201 })
138
+ }) as typeof fetch
139
+ const path = join(dir, 'mapping.txt')
140
+ await writeFile(path, 'foo.bar -> a.b:\n'.repeat(1000))
141
+ await uploadMapping({ ...ADMIN, path, release: 'r' })
142
+ expect(captured).toHaveLength(1)
143
+ expect(captured[0]?.name).toEndWith('.gz')
144
+ expect(captured[0]?.bytes[0]).toBe(0x1f)
145
+ expect(captured[0]?.bytes[1]).toBe(0x8b)
146
+ })
147
+
148
+ test('already-gzipped input passes through un-recompressed', async () => {
149
+ const { gzipSync } = await import('node:zlib')
150
+ const pre = gzipSync('already compressed')
151
+ const captured: Uint8Array[] = []
152
+ globalThis.fetch = (async (_url: Request | string | URL, init?: RequestInit) => {
153
+ const form = init?.body as FormData
154
+ const file = form.get('file') as File
155
+ captured.push(new Uint8Array(await file.arrayBuffer()))
156
+ return new Response('{}', { status: 201 })
157
+ }) as typeof fetch
158
+ const path = join(dir, 'mapping.txt.gz')
159
+ await writeFile(path, pre)
160
+ await uploadMapping({ ...ADMIN, path, release: 'r' })
161
+ expect(Buffer.from(captured[0] ?? [])).toEqual(pre)
162
+ })
163
+ })
@@ -0,0 +1,41 @@
1
+ // The probe scanner: finds sentori.probe('REF') call sites, skips
2
+ // junk dirs, dedupes.
3
+
4
+ import { mkdtemp, mkdir, rm, writeFile } from 'node:fs/promises'
5
+ import { tmpdir } from 'node:os'
6
+ import { join } from 'node:path'
7
+
8
+ import { afterEach, beforeEach, describe, expect, test } from 'bun:test'
9
+
10
+ import { scanProbes } from '../probes.js'
11
+
12
+ let dir = ''
13
+ beforeEach(async () => {
14
+ dir = await mkdtemp(join(tmpdir(), 'sentori-probes-'))
15
+ })
16
+ afterEach(async () => {
17
+ await rm(dir, { force: true, recursive: true })
18
+ })
19
+
20
+ describe('scanProbes', () => {
21
+ test('finds refs across quote styles and dedupes', async () => {
22
+ await writeFile(
23
+ join(dir, 'a.ts'),
24
+ `sentori.probe('SENT-1'); probe("SENT-2", {x:1}); sentori.probe(\`SENT-1\`)`,
25
+ )
26
+ await writeFile(join(dir, 'b.tsx'), `if (bad) { sentori.probe('SENT-3') }`)
27
+ expect(scanProbes(dir)).toEqual(['SENT-1', 'SENT-2', 'SENT-3'])
28
+ })
29
+
30
+ test('skips node_modules and non-source files', async () => {
31
+ await mkdir(join(dir, 'node_modules'), { recursive: true })
32
+ await writeFile(join(dir, 'node_modules', 'x.ts'), `probe('NOPE')`)
33
+ await writeFile(join(dir, 'notes.md'), `probe('ALSO-NOPE')`)
34
+ expect(scanProbes(dir)).toEqual([])
35
+ })
36
+
37
+ test('dynamic refs are invisible (documented limitation)', async () => {
38
+ await writeFile(join(dir, 'c.ts'), 'sentori.probe(ref)')
39
+ expect(scanProbes(dir)).toEqual([])
40
+ })
41
+ })