@goliapkg/sentori-cli 0.6.0 → 1.0.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 +268 -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 +30 -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 +10 -21
  27. package/lib/upload.d.ts.map +1 -1
  28. package/lib/upload.js +20 -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 +5 -12
  34. package/src/__tests__/probes.test.ts +41 -0
  35. package/src/index.ts +290 -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 +37 -44
  40. package/src/probes.ts +75 -0
  41. package/src/react-native.ts +16 -9
  42. package/src/upload.ts +30 -71
  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":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,oBAAoB,CAAA;AAC9C,OAAO,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,EAAE,MAAM,SAAS,CAAA;AACzD,OAAO,EAAE,aAAa,EAAE,MAAM,aAAa,CAAA;AAC3C,OAAO,EAAE,MAAM,EAAE,MAAM,SAAS,CAAA;AAChC,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAA;AAEhC,OAAO,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAA;AAE9C;;yDAEyD;AACzD,MAAM,UAAU,oBAAoB,CAAC,OAAO,GAAG,OAAO,CAAC,GAAG,EAAE;IAC1D,MAAM,GAAG,GAAG,aAAa,CAAC,IAAI,CAAC,OAAO,EAAE,SAAS,CAAC,CAAC,CAAA;IACnD,KAAK,MAAM,EAAE,IAAI;QACf,6CAA6C;QAC7C,oEAAoE;KACrE,EAAE,CAAC;QACF,IAAI,CAAC;YACH,MAAM,CAAC,GAAG,GAAG,CAAC,OAAO,CAAC,EAAE,CAAC,CAAA;YACzB,IAAI,UAAU,CAAC,CAAC,CAAC;gBAAE,OAAO,CAAC,CAAA;QAC7B,CAAC;QAAC,MAAM,CAAC;YACP,WAAW;QACb,CAAC;IACH,CAAC;IACD,OAAO,IAAI,CAAA;AACb,CAAC;AAED;;uEAEuE;AACvE,MAAM,UAAU,iBAAiB,CAAC,QAAgB,EAAE,SAAiB;IACnE,KAAK,MAAM,CAAC,IAAI,CAAC,QAAQ,EAAE,SAAS,CAAC,EAAE,CAAC;QACtC,IAAI,CAAC,UAAU,CAAC,CAAC,CAAC;YAAE,MAAM,IAAI,KAAK,CAAC,iBAAiB,CAAC,EAAE,CAAC,CAAA;IAC3D,CAAC;IACD,MAAM,MAAM,GAAG,oBAAoB,EAAE,CAAA;IACrC,IAAI,CAAC,MAAM,EAAE,CAAC;QACZ,MAAM,IAAI,KAAK,CACb,iFAAiF;YAC/E,iFAAiF,CACpF,CAAA;IACH,CAAC;IACD,MAAM,GAAG,GAAG,IAAI,CAAC,WAAW,CAAC,IAAI,CAAC,MAAM,EAAE,EAAE,aAAa,CAAC,CAAC,EAAE,cAAc,CAAC,CAAA;IAC5E,MAAM,CAAC,GAAG,SAAS,CAAC,MAAM,EAAE,CAAC,MAAM,EAAE,QAAQ,EAAE,SAAS,EAAE,IAAI,EAAE,GAAG,CAAC,EAAE,EAAE,KAAK,EAAE,SAAS,EAAE,CAAC,CAAA;IAC3F,IAAI,CAAC,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACnB,MAAM,IAAI,KAAK,CAAC,sCAAsC,CAAC,CAAC,MAAM,IAAI,QAAQ,EAAE,CAAC,CAAA;IAC/E,CAAC;IACD,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC;QAAE,MAAM,IAAI,KAAK,CAAC,2CAA2C,CAAC,CAAA;IAClF,OAAO,GAAG,CAAA;AACZ,CAAC;AAaD;0DAC0D;AAC1D,MAAM,CAAC,KAAK,UAAU,iBAAiB,CAAC,IAAqB;IAI3D,MAAM,QAAQ,GAAG,iBAAiB,CAAC,IAAI,CAAC,QAAQ,EAAE,IAAI,CAAC,SAAS,CAAC,CAAA;IACjE,IAAI,CAAC;QACH,MAAM,KAAK,GAAG,CAAC,QAAQ,EAAE,GAAG,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAA;QAC/D,MAAM,CAAC,GAAG,MAAM,gBAAgB,CAAC;YAC/B,MAAM,EAAE,IAAI,CAAC,MAAM;YACnB,MAAM,EAAE,IAAI,CAAC,MAAM;YACnB,KAAK;YACL,OAAO,EAAE,IAAI,CAAC,OAAO;YACrB,KAAK,EAAE,IAAI,CAAC,KAAK;SAClB,CAAC,CAAA;QACF,OAAO,EAAE,KAAK,EAAE,CAAC,CAAC,KAAK,EAAE,QAAQ,EAAE,CAAC,CAAC,QAAQ,EAAE,CAAA;IACjD,CAAC;YAAS,CAAC;QACT,IAAI,CAAC;YACH,MAAM,CAAC,QAAQ,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAA;YACjC,MAAM,CAAC,IAAI,CAAC,QAAQ,EAAE,IAAI,CAAC,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAA;QAChE,CAAC;QAAC,MAAM,CAAC;YACP,sBAAsB;QACxB,CAAC;IACH,CAAC;AACH,CAAC"}
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,13 @@
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;
9
- };
10
- export type UploadResult = {
11
- files: string[];
12
- uploaded?: number;
13
- artifacts?: {
14
- kind: string;
15
- name: string;
16
- }[];
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;
17
9
  };
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>;
10
+ export declare function uploadArtifact(opts: UploadOpts): Promise<{
11
+ id: string;
12
+ }>;
24
13
  //# 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":"AAUA,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;AAED,wBAAsB,cAAc,CAAC,IAAI,EAAE,UAAU,GAAG,OAAO,CAAC;IAAE,EAAE,EAAE,MAAM,CAAA;CAAE,CAAC,CAuB9E"}
package/lib/upload.js CHANGED
@@ -1,65 +1,28 @@
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
- }
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;
35
- }
36
- export async function uploadSourcemaps(opts) {
37
- const files = await collectFiles(opts.paths);
38
- if (opts.dryRun)
39
- return { files };
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
+ import { readFileSync } from 'node:fs';
8
+ import { basename } from 'node:path';
9
+ export async function uploadArtifact(opts) {
10
+ const bytes = readFileSync(opts.path);
11
+ if (bytes.length === 0)
12
+ throw new Error(`empty file: ${opts.path}`);
40
13
  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));
44
- }
45
- const base = opts.apiUrl.replace(/\/+$/, '');
46
- const url = `${base}/admin/api/releases/${encodeURIComponent(opts.release)}/sourcemaps`;
14
+ form.append('kind', opts.kind);
15
+ form.append('file', new Blob([new Uint8Array(bytes)]), opts.name ?? basename(opts.path));
16
+ const url = `${opts.apiUrl.replace(/\/+$/, '')}/v1/releases/${encodeURIComponent(opts.release)}/artifacts`;
47
17
  const resp = await fetch(url, {
48
- body: form,
49
- headers: { Authorization: `Bearer ${opts.token}` },
50
18
  method: 'POST',
19
+ headers: { Authorization: `Bearer ${opts.token}` },
20
+ body: form,
51
21
  });
52
22
  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)}` : ''}`);
23
+ const detail = await resp.text().catch(() => '');
24
+ throw new Error(`${resp.status} ${resp.statusText}${detail ? ` — ${detail.slice(0, 200)}` : ''}`);
61
25
  }
62
- const body = (await resp.json().catch(() => ({})));
63
- return { artifacts: body.artifacts, files, uploaded: body.uploaded ?? files.length };
26
+ return (await resp.json());
64
27
  }
65
28
  //# 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":"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"}
1
+ {"version":3,"file":"upload.js","sourceRoot":"","sources":["../src/upload.ts"],"names":[],"mappings":"AAAA,qEAAqE;AACrE,iEAAiE;AACjE,kEAAkE;AAClE,iEAAiE;AACjE,6DAA6D;AAC7D,WAAW;AAEX,OAAO,EAAE,YAAY,EAAE,MAAM,SAAS,CAAA;AACtC,OAAO,EAAE,QAAQ,EAAE,MAAM,WAAW,CAAA;AAYpC,MAAM,CAAC,KAAK,UAAU,cAAc,CAAC,IAAgB;IACnD,MAAM,KAAK,GAAG,YAAY,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;IACrC,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC;QAAE,MAAM,IAAI,KAAK,CAAC,eAAe,IAAI,CAAC,IAAI,EAAE,CAAC,CAAA;IAEnE,MAAM,IAAI,GAAG,IAAI,QAAQ,EAAE,CAAA;IAC3B,IAAI,CAAC,MAAM,CAAC,MAAM,EAAE,IAAI,CAAC,IAAI,CAAC,CAAA;IAC9B,IAAI,CAAC,MAAM,CACT,MAAM,EACN,IAAI,IAAI,CAAC,CAAC,IAAI,UAAU,CAAC,KAAK,CAAC,CAAC,CAAC,EACjC,IAAI,CAAC,IAAI,IAAI,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC,CACjC,CAAA;IAED,MAAM,GAAG,GAAG,GAAG,IAAI,CAAC,MAAM,CAAC,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC,gBAAgB,kBAAkB,CAAC,IAAI,CAAC,OAAO,CAAC,YAAY,CAAA;IAC1G,MAAM,IAAI,GAAG,MAAM,KAAK,CAAC,GAAG,EAAE;QAC5B,MAAM,EAAE,MAAM;QACd,OAAO,EAAE,EAAE,aAAa,EAAE,UAAU,IAAI,CAAC,KAAK,EAAE,EAAE;QAClD,IAAI,EAAE,IAAI;KACX,CAAC,CAAA;IACF,IAAI,CAAC,IAAI,CAAC,EAAE,EAAE,CAAC;QACb,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,IAAI,EAAE,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,EAAE,CAAC,CAAA;QAChD,MAAM,IAAI,KAAK,CAAC,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,CAAC,CAAA;IACnG,CAAC;IACD,OAAO,CAAC,MAAM,IAAI,CAAC,IAAI,EAAE,CAAmB,CAAA;AAC9C,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@goliapkg/sentori-cli",
3
- "version": "0.6.0",
3
+ "version": "1.0.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
  })
@@ -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
+ })