@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.
- package/lib/index.js +268 -340
- package/lib/index.js.map +1 -1
- package/lib/issue.d.ts +16 -18
- package/lib/issue.d.ts.map +1 -1
- package/lib/issue.js +29 -32
- package/lib/issue.js.map +1 -1
- package/lib/lenient.d.ts +13 -0
- package/lib/lenient.d.ts.map +1 -0
- package/lib/lenient.js +25 -0
- package/lib/lenient.js.map +1 -0
- package/lib/mcp.d.ts +4 -2
- package/lib/mcp.d.ts.map +1 -1
- package/lib/mcp.js +43 -220
- package/lib/mcp.js.map +1 -1
- package/lib/native-artifacts.d.ts +0 -1
- package/lib/native-artifacts.d.ts.map +1 -1
- package/lib/native-artifacts.js +30 -41
- package/lib/native-artifacts.js.map +1 -1
- package/lib/probes.d.ts +10 -0
- package/lib/probes.d.ts.map +1 -0
- package/lib/probes.js +75 -0
- package/lib/probes.js.map +1 -0
- package/lib/react-native.d.ts.map +1 -1
- package/lib/react-native.js +17 -9
- package/lib/react-native.js.map +1 -1
- package/lib/upload.d.ts +10 -21
- package/lib/upload.d.ts.map +1 -1
- package/lib/upload.js +20 -57
- package/lib/upload.js.map +1 -1
- package/package.json +2 -2
- package/src/__tests__/lenient.test.ts +32 -0
- package/src/__tests__/mcp.test.ts +20 -112
- package/src/__tests__/native-artifacts.test.ts +5 -12
- package/src/__tests__/probes.test.ts +41 -0
- package/src/index.ts +290 -362
- package/src/issue.ts +47 -46
- package/src/lenient.ts +39 -0
- package/src/mcp.ts +46 -215
- package/src/native-artifacts.ts +37 -44
- package/src/probes.ts +75 -0
- package/src/react-native.ts +16 -9
- package/src/upload.ts +30 -71
- package/src/__tests__/issue.test.ts +0 -95
- package/src/__tests__/source-bundle-from-dir.test.ts +0 -85
- package/src/__tests__/source-bundle.test.ts +0 -105
- package/src/__tests__/upload.test.ts +0 -121
- package/src/source-bundle.ts +0 -234
package/lib/react-native.js
CHANGED
|
@@ -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 {
|
|
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
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
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 {
|
package/lib/react-native.js.map
CHANGED
|
@@ -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,
|
|
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
|
|
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
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
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
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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
|
package/lib/upload.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"upload.d.ts","sourceRoot":"","sources":["../src/upload.ts"],"names":[],"mappings":"
|
|
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
|
-
|
|
2
|
-
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
// the
|
|
6
|
-
//
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
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
|
-
|
|
42
|
-
|
|
43
|
-
|
|
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
|
-
|
|
54
|
-
|
|
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
|
-
|
|
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":"AAAA,
|
|
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.
|
|
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": "^
|
|
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
|
-
|
|
1
|
+
// The MCP tool surface mirrors the /api closed loop 1:1.
|
|
2
2
|
|
|
3
|
-
import {
|
|
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
|
-
|
|
15
|
-
calls = []
|
|
16
|
-
})
|
|
17
|
-
afterEach(() => {
|
|
18
|
-
globalThis.fetch = origFetch
|
|
19
|
-
})
|
|
5
|
+
import { buildTools } from '../mcp.js'
|
|
20
6
|
|
|
21
|
-
|
|
22
|
-
|
|
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
|
-
|
|
35
|
-
|
|
36
|
-
|
|
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
|
-
'
|
|
41
|
-
'
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
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
|
-
|
|
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
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
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
|
-
|
|
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/
|
|
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]?.
|
|
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('/
|
|
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
|
+
})
|