@skillsmith/mcp-server 0.5.2 → 0.5.3
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 +24 -0
- package/dist/.tsbuildinfo +1 -1
- package/dist/src/__tests__/recommend-online-path.test.js +2 -0
- package/dist/src/__tests__/recommend-online-path.test.js.map +1 -1
- package/dist/src/__tests__/recommend.test.js +10 -2
- package/dist/src/__tests__/recommend.test.js.map +1 -1
- package/dist/src/__tests__/search-installable.test.js +5 -0
- package/dist/src/__tests__/search-installable.test.js.map +1 -1
- package/dist/src/__tests__/search-online-path.test.js +1 -0
- package/dist/src/__tests__/search-online-path.test.js.map +1 -1
- package/dist/src/__tests__/search.test.js +3 -0
- package/dist/src/__tests__/search.test.js.map +1 -1
- package/dist/src/index.js +9 -50
- package/dist/src/index.js.map +1 -1
- package/dist/src/middleware/license.gate.d.ts +2 -2
- package/dist/src/middleware/license.gate.d.ts.map +1 -1
- package/dist/src/middleware/license.gate.js +43 -1
- package/dist/src/middleware/license.gate.js.map +1 -1
- package/dist/src/middleware/telemetry-consent.d.ts +88 -0
- package/dist/src/middleware/telemetry-consent.d.ts.map +1 -0
- package/dist/src/middleware/telemetry-consent.js +177 -0
- package/dist/src/middleware/telemetry-consent.js.map +1 -0
- package/dist/src/middleware/telemetry-consent.test.d.ts +12 -0
- package/dist/src/middleware/telemetry-consent.test.d.ts.map +1 -0
- package/dist/src/middleware/telemetry-consent.test.js +291 -0
- package/dist/src/middleware/telemetry-consent.test.js.map +1 -0
- package/dist/src/tools/__meta__/telemetry-coverage.test.d.ts +25 -0
- package/dist/src/tools/__meta__/telemetry-coverage.test.d.ts.map +1 -0
- package/dist/src/tools/__meta__/telemetry-coverage.test.js +210 -0
- package/dist/src/tools/__meta__/telemetry-coverage.test.js.map +1 -0
- package/dist/src/tools/analytics.d.ts +15 -28
- package/dist/src/tools/analytics.d.ts.map +1 -1
- package/dist/src/tools/analytics.js +26 -4
- package/dist/src/tools/analytics.js.map +1 -1
- package/dist/src/tools/analytics.supabase.service.d.ts +103 -0
- package/dist/src/tools/analytics.supabase.service.d.ts.map +1 -0
- package/dist/src/tools/analytics.supabase.service.js +171 -0
- package/dist/src/tools/analytics.supabase.service.js.map +1 -0
- package/dist/src/tools/analytics.supabase.service.test.d.ts +10 -0
- package/dist/src/tools/analytics.supabase.service.test.d.ts.map +1 -0
- package/dist/src/tools/analytics.supabase.service.test.js +375 -0
- package/dist/src/tools/analytics.supabase.service.test.js.map +1 -0
- package/dist/src/tools/analyze.d.ts +6 -19
- package/dist/src/tools/analyze.d.ts.map +1 -1
- package/dist/src/tools/analyze.js +8 -1
- package/dist/src/tools/analyze.js.map +1 -1
- package/dist/src/tools/apply-namespace-rename.d.ts +1 -8
- package/dist/src/tools/apply-namespace-rename.d.ts.map +1 -1
- package/dist/src/tools/apply-namespace-rename.js +8 -1
- package/dist/src/tools/apply-namespace-rename.js.map +1 -1
- package/dist/src/tools/apply-recommended-edit.d.ts +1 -6
- package/dist/src/tools/apply-recommended-edit.d.ts.map +1 -1
- package/dist/src/tools/apply-recommended-edit.js +8 -1
- package/dist/src/tools/apply-recommended-edit.js.map +1 -1
- package/dist/src/tools/audit-tools.d.ts +20 -3
- package/dist/src/tools/audit-tools.d.ts.map +1 -1
- package/dist/src/tools/audit-tools.js +20 -3
- package/dist/src/tools/audit-tools.js.map +1 -1
- package/dist/src/tools/compare.d.ts +5 -21
- package/dist/src/tools/compare.d.ts.map +1 -1
- package/dist/src/tools/compare.js +8 -1
- package/dist/src/tools/compare.js.map +1 -1
- package/dist/src/tools/compliance-tools.d.ts +5 -1
- package/dist/src/tools/compliance-tools.d.ts.map +1 -1
- package/dist/src/tools/compliance-tools.js +8 -1
- package/dist/src/tools/compliance-tools.js.map +1 -1
- package/dist/src/tools/get-skill.d.ts +1 -20
- package/dist/src/tools/get-skill.d.ts.map +1 -1
- package/dist/src/tools/get-skill.js +8 -1
- package/dist/src/tools/get-skill.js.map +1 -1
- package/dist/src/tools/index-local.d.ts +1 -15
- package/dist/src/tools/index-local.d.ts.map +1 -1
- package/dist/src/tools/index-local.js +8 -1
- package/dist/src/tools/index-local.js.map +1 -1
- package/dist/src/tools/install.d.ts +1 -17
- package/dist/src/tools/install.d.ts.map +1 -1
- package/dist/src/tools/install.js +8 -1
- package/dist/src/tools/install.js.map +1 -1
- package/dist/src/tools/integration-tools.d.ts +14 -2
- package/dist/src/tools/integration-tools.d.ts.map +1 -1
- package/dist/src/tools/integration-tools.js +14 -2
- package/dist/src/tools/integration-tools.js.map +1 -1
- package/dist/src/tools/outdated.d.ts +3 -14
- package/dist/src/tools/outdated.d.ts.map +1 -1
- package/dist/src/tools/outdated.js +8 -1
- package/dist/src/tools/outdated.js.map +1 -1
- package/dist/src/tools/outdated.test.js +3 -0
- package/dist/src/tools/outdated.test.js.map +1 -1
- package/dist/src/tools/publish-private.d.ts +4 -7
- package/dist/src/tools/publish-private.d.ts.map +1 -1
- package/dist/src/tools/publish-private.js +8 -1
- package/dist/src/tools/publish-private.js.map +1 -1
- package/dist/src/tools/publish.d.ts +9 -11
- package/dist/src/tools/publish.d.ts.map +1 -1
- package/dist/src/tools/publish.js +8 -1
- package/dist/src/tools/publish.js.map +1 -1
- package/dist/src/tools/rbac-tools.d.ts +20 -3
- package/dist/src/tools/rbac-tools.d.ts.map +1 -1
- package/dist/src/tools/rbac-tools.js +20 -3
- package/dist/src/tools/rbac-tools.js.map +1 -1
- package/dist/src/tools/rbac-tools.test.js +2 -0
- package/dist/src/tools/rbac-tools.test.js.map +1 -1
- package/dist/src/tools/recommend.d.ts +9 -9
- package/dist/src/tools/recommend.d.ts.map +1 -1
- package/dist/src/tools/recommend.js +8 -1
- package/dist/src/tools/recommend.js.map +1 -1
- package/dist/src/tools/registry-tools.d.ts +10 -8
- package/dist/src/tools/registry-tools.d.ts.map +1 -1
- package/dist/src/tools/registry-tools.js +14 -2
- package/dist/src/tools/registry-tools.js.map +1 -1
- package/dist/src/tools/search.d.ts +3 -42
- package/dist/src/tools/search.d.ts.map +1 -1
- package/dist/src/tools/search.js +10 -24
- package/dist/src/tools/search.js.map +1 -1
- package/dist/src/tools/skill-audit.d.ts +3 -13
- package/dist/src/tools/skill-audit.d.ts.map +1 -1
- package/dist/src/tools/skill-audit.js +8 -1
- package/dist/src/tools/skill-audit.js.map +1 -1
- package/dist/src/tools/skill-diff.d.ts +9 -11
- package/dist/src/tools/skill-diff.d.ts.map +1 -1
- package/dist/src/tools/skill-diff.js +8 -1
- package/dist/src/tools/skill-diff.js.map +1 -1
- package/dist/src/tools/skill-inventory-audit.d.ts +1 -6
- package/dist/src/tools/skill-inventory-audit.d.ts.map +1 -1
- package/dist/src/tools/skill-inventory-audit.js +8 -1
- package/dist/src/tools/skill-inventory-audit.js.map +1 -1
- package/dist/src/tools/skill-pack-audit.d.ts +4 -12
- package/dist/src/tools/skill-pack-audit.d.ts.map +1 -1
- package/dist/src/tools/skill-pack-audit.js +8 -1
- package/dist/src/tools/skill-pack-audit.js.map +1 -1
- package/dist/src/tools/skill-rescan.d.ts +3 -11
- package/dist/src/tools/skill-rescan.d.ts.map +1 -1
- package/dist/src/tools/skill-rescan.js +8 -1
- package/dist/src/tools/skill-rescan.js.map +1 -1
- package/dist/src/tools/skill-rescan.test.js +2 -0
- package/dist/src/tools/skill-rescan.test.js.map +1 -1
- package/dist/src/tools/skill-updates.d.ts +3 -12
- package/dist/src/tools/skill-updates.d.ts.map +1 -1
- package/dist/src/tools/skill-updates.js +8 -1
- package/dist/src/tools/skill-updates.js.map +1 -1
- package/dist/src/tools/sso-tools.d.ts +9 -8
- package/dist/src/tools/sso-tools.d.ts.map +1 -1
- package/dist/src/tools/sso-tools.js +14 -2
- package/dist/src/tools/sso-tools.js.map +1 -1
- package/dist/src/tools/suggest.d.ts +9 -17
- package/dist/src/tools/suggest.d.ts.map +1 -1
- package/dist/src/tools/suggest.js +8 -1
- package/dist/src/tools/suggest.js.map +1 -1
- package/dist/src/tools/team-workspace.d.ts +11 -16
- package/dist/src/tools/team-workspace.d.ts.map +1 -1
- package/dist/src/tools/team-workspace.js +14 -2
- package/dist/src/tools/team-workspace.js.map +1 -1
- package/dist/src/tools/uninstall.d.ts +4 -10
- package/dist/src/tools/uninstall.d.ts.map +1 -1
- package/dist/src/tools/uninstall.js +8 -1
- package/dist/src/tools/uninstall.js.map +1 -1
- package/dist/src/tools/validate.d.ts +5 -23
- package/dist/src/tools/validate.d.ts.map +1 -1
- package/dist/src/tools/validate.js +8 -1
- package/dist/src/tools/validate.js.map +1 -1
- package/dist/src/validation.d.ts +2 -2
- package/dist/src/validation.d.ts.map +1 -1
- package/dist/src/validation.js +6 -0
- package/dist/src/validation.js.map +1 -1
- package/dist/src/webhooks/stripe-webhook-endpoint.d.ts +24 -14
- package/dist/src/webhooks/stripe-webhook-endpoint.d.ts.map +1 -1
- package/dist/src/webhooks/stripe-webhook-endpoint.js.map +1 -1
- package/dist/tests/compare.test.js +3 -0
- package/dist/tests/compare.test.js.map +1 -1
- package/dist/tests/e2e/compare.e2e.test.js +3 -1
- package/dist/tests/e2e/compare.e2e.test.js.map +1 -1
- package/dist/tests/e2e/recommend.e2e.test.js +6 -1
- package/dist/tests/e2e/recommend.e2e.test.js.map +1 -1
- package/dist/tests/e2e/skill-flow.e2e.test.js +8 -0
- package/dist/tests/e2e/skill-flow.e2e.test.js.map +1 -1
- package/dist/tests/e2e/suggest.e2e.test.js +5 -1
- package/dist/tests/e2e/suggest.e2e.test.js.map +1 -1
- package/dist/tests/integration/analyze.integration.test.js +7 -0
- package/dist/tests/integration/analyze.integration.test.js.map +1 -1
- package/dist/tests/integration/recommend.integration.test.js +6 -0
- package/dist/tests/integration/recommend.integration.test.js.map +1 -1
- package/dist/tests/integration/validate.integration.test.js +7 -1
- package/dist/tests/integration/validate.integration.test.js.map +1 -1
- package/dist/tests/performance/search-performance.test.js +9 -1
- package/dist/tests/performance/search-performance.test.js.map +1 -1
- package/dist/tests/recommend.test.js +11 -2
- package/dist/tests/recommend.test.js.map +1 -1
- package/dist/tests/startup-probe.test.js +9 -4
- package/dist/tests/startup-probe.test.js.map +1 -1
- package/dist/tests/unit/skill-pack-audit.test.js +14 -1
- package/dist/tests/unit/skill-pack-audit.test.js.map +1 -1
- package/dist/tests/validate.test.js +28 -8
- package/dist/tests/validate.test.js.map +1 -1
- package/package.json +2 -2
- package/server.json +2 -2
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Telemetry Consent Gate — SMI-5019 W2.S4
|
|
3
|
+
*
|
|
4
|
+
* For MCP-only clients (Cursor, Continue, Copilot users without a CLI install)
|
|
5
|
+
* we cannot rely on a CLI first-run prompt or a VS Code toast. Per user
|
|
6
|
+
* decision U5 in the implementation plan, the consent surface is the web
|
|
7
|
+
* dashboard at https://skillsmith.app/account/telemetry.
|
|
8
|
+
*
|
|
9
|
+
* This module supplies the MCP-side half of that flow:
|
|
10
|
+
*
|
|
11
|
+
* 1. On every tool call, resolve the calling anonymous_id's preference from
|
|
12
|
+
* `user_telemetry_preferences` (RLS-scoped via the same anon-key client
|
|
13
|
+
* used elsewhere in this package).
|
|
14
|
+
* 2. If the row is missing, signal `consent_required:true` + the privacy URL
|
|
15
|
+
* in the response envelope so the client can prompt the user to open the
|
|
16
|
+
* dashboard.
|
|
17
|
+
* 3. Cache the resolved state per process (Map keyed by anonymous_id) so
|
|
18
|
+
* repeated calls within a session don't re-query, and so two parallel
|
|
19
|
+
* calls from the same unrecognized anonymous_id observe identical state.
|
|
20
|
+
* 4. Suppress telemetry writes (consult `shouldEmitTelemetry`) for that
|
|
21
|
+
* anonymous_id until the preference resolves to `enabled:true`.
|
|
22
|
+
*
|
|
23
|
+
* SMI-5016 (`packages/core/src/telemetry/wrap.ts`) and SMI-5017 (tool /
|
|
24
|
+
* command dispatchers) are wave-sibling deliverables — this module
|
|
25
|
+
* deliberately stays out of those files.
|
|
26
|
+
*/
|
|
27
|
+
/**
|
|
28
|
+
* Canonical absolute URL of the consent dashboard. Must remain stable across
|
|
29
|
+
* surfaces so MCP clients can deep-link to a known landing page.
|
|
30
|
+
*/
|
|
31
|
+
export declare const TELEMETRY_PRIVACY_URL = "https://skillsmith.app/account/telemetry";
|
|
32
|
+
/**
|
|
33
|
+
* Result of resolving the consent state for a given anonymous_id.
|
|
34
|
+
*/
|
|
35
|
+
export interface ConsentState {
|
|
36
|
+
/** True iff a preference row was found AND `enabled = true`. */
|
|
37
|
+
enabled: boolean;
|
|
38
|
+
/**
|
|
39
|
+
* True iff there is no row for this anonymous_id yet (the user hasn't
|
|
40
|
+
* visited the consent page). Surface this in the response envelope so the
|
|
41
|
+
* client can prompt the user.
|
|
42
|
+
*/
|
|
43
|
+
consentRequired: boolean;
|
|
44
|
+
/** Stable URL to direct the user to when `consentRequired` is true. */
|
|
45
|
+
privacyUrl: string;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Resolve the consent state for `anonymousId`, caching the result for the
|
|
49
|
+
* lifetime of the process. Concurrent calls share one in-flight query.
|
|
50
|
+
*
|
|
51
|
+
* Passing `null`/`undefined`/empty triggers the no-id branch — telemetry is
|
|
52
|
+
* suppressed but no prompt is shown (there's nothing to link the user's
|
|
53
|
+
* eventual web-dashboard choice back to).
|
|
54
|
+
*/
|
|
55
|
+
export declare function resolveConsent(anonymousId: string | null | undefined): Promise<ConsentState>;
|
|
56
|
+
/**
|
|
57
|
+
* Convenience: true iff telemetry may be emitted for this anonymous_id.
|
|
58
|
+
* Wraps `resolveConsent` for callers that only need the boolean.
|
|
59
|
+
*/
|
|
60
|
+
export declare function shouldEmitTelemetry(anonymousId: string | null | undefined): Promise<boolean>;
|
|
61
|
+
/**
|
|
62
|
+
* Invalidate the cache entry for `anonymousId`. Called by the consent page
|
|
63
|
+
* after a successful save would, in a future iteration, ping an MCP refresh
|
|
64
|
+
* endpoint — for now this is exposed for tests and for the explicit
|
|
65
|
+
* resync-on-rotate UI in the consent page.
|
|
66
|
+
*/
|
|
67
|
+
export declare function invalidateConsentCache(anonymousId?: string): void;
|
|
68
|
+
/**
|
|
69
|
+
* Augment an existing MCP tool response with a `consent_required` envelope
|
|
70
|
+
* when the user has not yet visited the consent page.
|
|
71
|
+
*
|
|
72
|
+
* The MCP `CallToolResult` shape is `{ content: [{ type: 'text', text: <json> }] }`.
|
|
73
|
+
* We parse `text`, splice in the consent fields, and re-serialize. If parsing
|
|
74
|
+
* fails for any reason (binary content, malformed payload), we return the
|
|
75
|
+
* response untouched — telemetry consent is a soft signal and must never
|
|
76
|
+
* corrupt a successful tool result.
|
|
77
|
+
*
|
|
78
|
+
* Idempotent: calling this twice on the same response is a no-op once the
|
|
79
|
+
* fields are already present.
|
|
80
|
+
*/
|
|
81
|
+
export declare function annotateResponseWithConsent<T extends {
|
|
82
|
+
content?: unknown;
|
|
83
|
+
}>(response: T, consent: ConsentState): T;
|
|
84
|
+
/**
|
|
85
|
+
* Test-only helper. Not exported from the package index.
|
|
86
|
+
*/
|
|
87
|
+
export declare function _resetConsentCacheForTests(): void;
|
|
88
|
+
//# sourceMappingURL=telemetry-consent.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"telemetry-consent.d.ts","sourceRoot":"","sources":["../../../src/middleware/telemetry-consent.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AAIH;;;GAGG;AACH,eAAO,MAAM,qBAAqB,6CAA6C,CAAA;AAE/E;;GAEG;AACH,MAAM,WAAW,YAAY;IAC3B,gEAAgE;IAChE,OAAO,EAAE,OAAO,CAAA;IAChB;;;;OAIG;IACH,eAAe,EAAE,OAAO,CAAA;IACxB,uEAAuE;IACvE,UAAU,EAAE,MAAM,CAAA;CACnB;AA6ED;;;;;;;GAOG;AACH,wBAAgB,cAAc,CAAC,WAAW,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAAG,OAAO,CAAC,YAAY,CAAC,CAS5F;AAED;;;GAGG;AACH,wBAAsB,mBAAmB,CACvC,WAAW,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GACrC,OAAO,CAAC,OAAO,CAAC,CAIlB;AAED;;;;;GAKG;AACH,wBAAgB,sBAAsB,CAAC,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAMjE;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,2BAA2B,CAAC,CAAC,SAAS;IAAE,OAAO,CAAC,EAAE,OAAO,CAAA;CAAE,EACzE,QAAQ,EAAE,CAAC,EACX,OAAO,EAAE,YAAY,GACpB,CAAC,CA8BH;AAED;;GAEG;AACH,wBAAgB,0BAA0B,IAAI,IAAI,CAEjD"}
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Telemetry Consent Gate — SMI-5019 W2.S4
|
|
3
|
+
*
|
|
4
|
+
* For MCP-only clients (Cursor, Continue, Copilot users without a CLI install)
|
|
5
|
+
* we cannot rely on a CLI first-run prompt or a VS Code toast. Per user
|
|
6
|
+
* decision U5 in the implementation plan, the consent surface is the web
|
|
7
|
+
* dashboard at https://skillsmith.app/account/telemetry.
|
|
8
|
+
*
|
|
9
|
+
* This module supplies the MCP-side half of that flow:
|
|
10
|
+
*
|
|
11
|
+
* 1. On every tool call, resolve the calling anonymous_id's preference from
|
|
12
|
+
* `user_telemetry_preferences` (RLS-scoped via the same anon-key client
|
|
13
|
+
* used elsewhere in this package).
|
|
14
|
+
* 2. If the row is missing, signal `consent_required:true` + the privacy URL
|
|
15
|
+
* in the response envelope so the client can prompt the user to open the
|
|
16
|
+
* dashboard.
|
|
17
|
+
* 3. Cache the resolved state per process (Map keyed by anonymous_id) so
|
|
18
|
+
* repeated calls within a session don't re-query, and so two parallel
|
|
19
|
+
* calls from the same unrecognized anonymous_id observe identical state.
|
|
20
|
+
* 4. Suppress telemetry writes (consult `shouldEmitTelemetry`) for that
|
|
21
|
+
* anonymous_id until the preference resolves to `enabled:true`.
|
|
22
|
+
*
|
|
23
|
+
* SMI-5016 (`packages/core/src/telemetry/wrap.ts`) and SMI-5017 (tool /
|
|
24
|
+
* command dispatchers) are wave-sibling deliverables — this module
|
|
25
|
+
* deliberately stays out of those files.
|
|
26
|
+
*/
|
|
27
|
+
import { getSupabaseClient } from '../supabase-client.js';
|
|
28
|
+
/**
|
|
29
|
+
* Canonical absolute URL of the consent dashboard. Must remain stable across
|
|
30
|
+
* surfaces so MCP clients can deep-link to a known landing page.
|
|
31
|
+
*/
|
|
32
|
+
export const TELEMETRY_PRIVACY_URL = 'https://skillsmith.app/account/telemetry';
|
|
33
|
+
/**
|
|
34
|
+
* Per-process cache keyed by anonymous_id. We deliberately use a single shared
|
|
35
|
+
* Map so two parallel `withConsentGate` invocations for the same
|
|
36
|
+
* anonymous_id observe identical state — the constraint flagged in the spec.
|
|
37
|
+
*
|
|
38
|
+
* Stored value is a Promise (not the resolved ConsentState) so concurrent
|
|
39
|
+
* lookups share one in-flight Supabase query.
|
|
40
|
+
*/
|
|
41
|
+
const consentCache = new Map();
|
|
42
|
+
const DEFAULT_CONSENT_REQUIRED = {
|
|
43
|
+
enabled: false,
|
|
44
|
+
consentRequired: true,
|
|
45
|
+
privacyUrl: TELEMETRY_PRIVACY_URL,
|
|
46
|
+
};
|
|
47
|
+
const DEFAULT_NO_ID = {
|
|
48
|
+
enabled: false,
|
|
49
|
+
consentRequired: false,
|
|
50
|
+
privacyUrl: TELEMETRY_PRIVACY_URL,
|
|
51
|
+
};
|
|
52
|
+
/**
|
|
53
|
+
* Look up the consent row for `anonymousId` and translate it into a
|
|
54
|
+
* `ConsentState`. Falls back to "consent required" on any error so we never
|
|
55
|
+
* silently emit telemetry from an unknown identity.
|
|
56
|
+
*/
|
|
57
|
+
async function fetchConsentState(anonymousId) {
|
|
58
|
+
let client;
|
|
59
|
+
try {
|
|
60
|
+
client = (await getSupabaseClient());
|
|
61
|
+
}
|
|
62
|
+
catch {
|
|
63
|
+
// Supabase isn't configured in this environment (e.g. offline CLI run);
|
|
64
|
+
// safest interpretation is "no consent → no emit" but also "no need to
|
|
65
|
+
// prompt the user" because the network surface isn't reachable anyway.
|
|
66
|
+
return { ...DEFAULT_NO_ID };
|
|
67
|
+
}
|
|
68
|
+
try {
|
|
69
|
+
const { data, error } = await client
|
|
70
|
+
.from('user_telemetry_preferences')
|
|
71
|
+
.select('enabled')
|
|
72
|
+
.eq('anonymous_id', anonymousId)
|
|
73
|
+
.maybeSingle();
|
|
74
|
+
if (error || !data) {
|
|
75
|
+
return { ...DEFAULT_CONSENT_REQUIRED };
|
|
76
|
+
}
|
|
77
|
+
return {
|
|
78
|
+
enabled: data.enabled === true,
|
|
79
|
+
consentRequired: false,
|
|
80
|
+
privacyUrl: TELEMETRY_PRIVACY_URL,
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
catch {
|
|
84
|
+
return { ...DEFAULT_CONSENT_REQUIRED };
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Resolve the consent state for `anonymousId`, caching the result for the
|
|
89
|
+
* lifetime of the process. Concurrent calls share one in-flight query.
|
|
90
|
+
*
|
|
91
|
+
* Passing `null`/`undefined`/empty triggers the no-id branch — telemetry is
|
|
92
|
+
* suppressed but no prompt is shown (there's nothing to link the user's
|
|
93
|
+
* eventual web-dashboard choice back to).
|
|
94
|
+
*/
|
|
95
|
+
export function resolveConsent(anonymousId) {
|
|
96
|
+
if (!anonymousId) {
|
|
97
|
+
return Promise.resolve({ ...DEFAULT_NO_ID });
|
|
98
|
+
}
|
|
99
|
+
const cached = consentCache.get(anonymousId);
|
|
100
|
+
if (cached)
|
|
101
|
+
return cached;
|
|
102
|
+
const promise = fetchConsentState(anonymousId);
|
|
103
|
+
consentCache.set(anonymousId, promise);
|
|
104
|
+
return promise;
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Convenience: true iff telemetry may be emitted for this anonymous_id.
|
|
108
|
+
* Wraps `resolveConsent` for callers that only need the boolean.
|
|
109
|
+
*/
|
|
110
|
+
export async function shouldEmitTelemetry(anonymousId) {
|
|
111
|
+
if (!anonymousId)
|
|
112
|
+
return false;
|
|
113
|
+
const state = await resolveConsent(anonymousId);
|
|
114
|
+
return state.enabled;
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Invalidate the cache entry for `anonymousId`. Called by the consent page
|
|
118
|
+
* after a successful save would, in a future iteration, ping an MCP refresh
|
|
119
|
+
* endpoint — for now this is exposed for tests and for the explicit
|
|
120
|
+
* resync-on-rotate UI in the consent page.
|
|
121
|
+
*/
|
|
122
|
+
export function invalidateConsentCache(anonymousId) {
|
|
123
|
+
if (anonymousId === undefined) {
|
|
124
|
+
consentCache.clear();
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
consentCache.delete(anonymousId);
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* Augment an existing MCP tool response with a `consent_required` envelope
|
|
131
|
+
* when the user has not yet visited the consent page.
|
|
132
|
+
*
|
|
133
|
+
* The MCP `CallToolResult` shape is `{ content: [{ type: 'text', text: <json> }] }`.
|
|
134
|
+
* We parse `text`, splice in the consent fields, and re-serialize. If parsing
|
|
135
|
+
* fails for any reason (binary content, malformed payload), we return the
|
|
136
|
+
* response untouched — telemetry consent is a soft signal and must never
|
|
137
|
+
* corrupt a successful tool result.
|
|
138
|
+
*
|
|
139
|
+
* Idempotent: calling this twice on the same response is a no-op once the
|
|
140
|
+
* fields are already present.
|
|
141
|
+
*/
|
|
142
|
+
export function annotateResponseWithConsent(response, consent) {
|
|
143
|
+
if (!consent.consentRequired)
|
|
144
|
+
return response;
|
|
145
|
+
const content = response.content;
|
|
146
|
+
if (!Array.isArray(content) || content.length === 0)
|
|
147
|
+
return response;
|
|
148
|
+
const first = content[0];
|
|
149
|
+
if (!first || first.type !== 'text' || typeof first.text !== 'string')
|
|
150
|
+
return response;
|
|
151
|
+
let parsed;
|
|
152
|
+
try {
|
|
153
|
+
parsed = JSON.parse(first.text);
|
|
154
|
+
}
|
|
155
|
+
catch {
|
|
156
|
+
return response;
|
|
157
|
+
}
|
|
158
|
+
if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {
|
|
159
|
+
return response;
|
|
160
|
+
}
|
|
161
|
+
const annotated = parsed;
|
|
162
|
+
// Idempotency: if the caller has already added these, leave them alone.
|
|
163
|
+
if ('consent_required' in annotated && 'privacy_url' in annotated)
|
|
164
|
+
return response;
|
|
165
|
+
annotated.consent_required = true;
|
|
166
|
+
annotated.privacy_url = consent.privacyUrl;
|
|
167
|
+
const nextContent = [...content];
|
|
168
|
+
nextContent[0] = { ...first, text: JSON.stringify(annotated, null, 2) };
|
|
169
|
+
return { ...response, content: nextContent };
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* Test-only helper. Not exported from the package index.
|
|
173
|
+
*/
|
|
174
|
+
export function _resetConsentCacheForTests() {
|
|
175
|
+
consentCache.clear();
|
|
176
|
+
}
|
|
177
|
+
//# sourceMappingURL=telemetry-consent.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"telemetry-consent.js","sourceRoot":"","sources":["../../../src/middleware/telemetry-consent.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Tests for the telemetry consent gate — SMI-5019 W2
|
|
3
|
+
*
|
|
4
|
+
* Mocking style matches analytics.supabase.service.test.ts:
|
|
5
|
+
* vi.mock('../supabase-client.js') at module scope, then
|
|
6
|
+
* vi.mocked(getSupabaseClient).mockResolvedValue / mockRejectedValue per test.
|
|
7
|
+
*
|
|
8
|
+
* `_resetConsentCacheForTests()` is called in beforeEach so every test starts
|
|
9
|
+
* with an empty process-level cache.
|
|
10
|
+
*/
|
|
11
|
+
export {};
|
|
12
|
+
//# sourceMappingURL=telemetry-consent.test.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"telemetry-consent.test.d.ts","sourceRoot":"","sources":["../../../src/middleware/telemetry-consent.test.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG"}
|
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Tests for the telemetry consent gate — SMI-5019 W2
|
|
3
|
+
*
|
|
4
|
+
* Mocking style matches analytics.supabase.service.test.ts:
|
|
5
|
+
* vi.mock('../supabase-client.js') at module scope, then
|
|
6
|
+
* vi.mocked(getSupabaseClient).mockResolvedValue / mockRejectedValue per test.
|
|
7
|
+
*
|
|
8
|
+
* `_resetConsentCacheForTests()` is called in beforeEach so every test starts
|
|
9
|
+
* with an empty process-level cache.
|
|
10
|
+
*/
|
|
11
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
12
|
+
import { resolveConsent, shouldEmitTelemetry, invalidateConsentCache, annotateResponseWithConsent, _resetConsentCacheForTests, TELEMETRY_PRIVACY_URL, } from './telemetry-consent.js';
|
|
13
|
+
// ============================================================================
|
|
14
|
+
// Supabase module mock
|
|
15
|
+
// ============================================================================
|
|
16
|
+
vi.mock('../supabase-client.js', () => ({
|
|
17
|
+
getSupabaseClient: vi.fn(),
|
|
18
|
+
}));
|
|
19
|
+
import { getSupabaseClient } from '../supabase-client.js';
|
|
20
|
+
const mockGetClient = vi.mocked(getSupabaseClient);
|
|
21
|
+
// ============================================================================
|
|
22
|
+
// Helper — build a chainable Supabase query mock
|
|
23
|
+
// ============================================================================
|
|
24
|
+
/**
|
|
25
|
+
* Creates a mock Supabase client whose `.from().select().eq().maybeSingle()`
|
|
26
|
+
* chain resolves with `resolvedValue`. Returns the `maybeSingle` spy so
|
|
27
|
+
* callers can assert call counts.
|
|
28
|
+
*/
|
|
29
|
+
function createQueryMock(resolvedValue) {
|
|
30
|
+
const maybeSingle = vi.fn().mockResolvedValue(resolvedValue);
|
|
31
|
+
const eq = vi.fn().mockReturnValue({ maybeSingle });
|
|
32
|
+
const select = vi.fn().mockReturnValue({ eq });
|
|
33
|
+
const from = vi.fn().mockReturnValue({ select });
|
|
34
|
+
const client = { from };
|
|
35
|
+
return { client, from, select, eq, maybeSingle };
|
|
36
|
+
}
|
|
37
|
+
// ============================================================================
|
|
38
|
+
// Setup
|
|
39
|
+
// ============================================================================
|
|
40
|
+
beforeEach(() => {
|
|
41
|
+
_resetConsentCacheForTests();
|
|
42
|
+
vi.clearAllMocks();
|
|
43
|
+
});
|
|
44
|
+
afterEach(() => {
|
|
45
|
+
_resetConsentCacheForTests();
|
|
46
|
+
});
|
|
47
|
+
// ============================================================================
|
|
48
|
+
// (1) Default-no-id: empty / null / undefined anonymous_id
|
|
49
|
+
// ============================================================================
|
|
50
|
+
describe('resolveConsent — default-no-id branch', () => {
|
|
51
|
+
it('returns DEFAULT_NO_ID for empty string without querying Supabase', async () => {
|
|
52
|
+
const state = await resolveConsent('');
|
|
53
|
+
expect(state.enabled).toBe(false);
|
|
54
|
+
expect(state.consentRequired).toBe(false);
|
|
55
|
+
expect(state.privacyUrl).toBe(TELEMETRY_PRIVACY_URL);
|
|
56
|
+
expect(mockGetClient).not.toHaveBeenCalled();
|
|
57
|
+
});
|
|
58
|
+
it('returns DEFAULT_NO_ID for null without querying Supabase', async () => {
|
|
59
|
+
const state = await resolveConsent(null);
|
|
60
|
+
expect(state.enabled).toBe(false);
|
|
61
|
+
expect(state.consentRequired).toBe(false);
|
|
62
|
+
expect(mockGetClient).not.toHaveBeenCalled();
|
|
63
|
+
});
|
|
64
|
+
it('returns DEFAULT_NO_ID for undefined without querying Supabase', async () => {
|
|
65
|
+
const state = await resolveConsent(undefined);
|
|
66
|
+
expect(state.enabled).toBe(false);
|
|
67
|
+
expect(state.consentRequired).toBe(false);
|
|
68
|
+
expect(mockGetClient).not.toHaveBeenCalled();
|
|
69
|
+
});
|
|
70
|
+
it('shouldEmitTelemetry returns false for empty string', async () => {
|
|
71
|
+
expect(await shouldEmitTelemetry('')).toBe(false);
|
|
72
|
+
expect(mockGetClient).not.toHaveBeenCalled();
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
// ============================================================================
|
|
76
|
+
// (2) Unknown anonymous_id — no row in DB
|
|
77
|
+
// ============================================================================
|
|
78
|
+
describe('resolveConsent — unknown anonymous_id (no row)', () => {
|
|
79
|
+
it('returns consent_required: true when Supabase returns no row', async () => {
|
|
80
|
+
const { client } = createQueryMock({ data: null, error: null });
|
|
81
|
+
mockGetClient.mockResolvedValue(client);
|
|
82
|
+
const state = await resolveConsent('unknown-xyz');
|
|
83
|
+
expect(state.consentRequired).toBe(true);
|
|
84
|
+
expect(state.enabled).toBe(false);
|
|
85
|
+
expect(state.privacyUrl).toBe(TELEMETRY_PRIVACY_URL);
|
|
86
|
+
});
|
|
87
|
+
it('shouldEmitTelemetry returns false for unknown anonymous_id', async () => {
|
|
88
|
+
const { client } = createQueryMock({ data: null, error: null });
|
|
89
|
+
mockGetClient.mockResolvedValue(client);
|
|
90
|
+
expect(await shouldEmitTelemetry('unknown-xyz')).toBe(false);
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
// ============================================================================
|
|
94
|
+
// (3) Enabled preference
|
|
95
|
+
// ============================================================================
|
|
96
|
+
describe('resolveConsent — enabled preference', () => {
|
|
97
|
+
it('returns consentRequired: false and enabled: true when row has enabled=true', async () => {
|
|
98
|
+
const { client } = createQueryMock({ data: { enabled: true }, error: null });
|
|
99
|
+
mockGetClient.mockResolvedValue(client);
|
|
100
|
+
const state = await resolveConsent('user-abc');
|
|
101
|
+
expect(state.enabled).toBe(true);
|
|
102
|
+
expect(state.consentRequired).toBe(false);
|
|
103
|
+
expect(state.privacyUrl).toBe(TELEMETRY_PRIVACY_URL);
|
|
104
|
+
});
|
|
105
|
+
it('shouldEmitTelemetry returns true when preference is enabled', async () => {
|
|
106
|
+
const { client } = createQueryMock({ data: { enabled: true }, error: null });
|
|
107
|
+
mockGetClient.mockResolvedValue(client);
|
|
108
|
+
expect(await shouldEmitTelemetry('user-abc')).toBe(true);
|
|
109
|
+
});
|
|
110
|
+
});
|
|
111
|
+
// ============================================================================
|
|
112
|
+
// (4) Disabled preference
|
|
113
|
+
// ============================================================================
|
|
114
|
+
describe('resolveConsent — disabled preference', () => {
|
|
115
|
+
it('returns consentRequired: false and enabled: false when row has enabled=false', async () => {
|
|
116
|
+
const { client } = createQueryMock({ data: { enabled: false }, error: null });
|
|
117
|
+
mockGetClient.mockResolvedValue(client);
|
|
118
|
+
const state = await resolveConsent('user-abc');
|
|
119
|
+
// User has answered — no prompt needed, but telemetry is off.
|
|
120
|
+
expect(state.consentRequired).toBe(false);
|
|
121
|
+
expect(state.enabled).toBe(false);
|
|
122
|
+
});
|
|
123
|
+
it('shouldEmitTelemetry returns false when preference is disabled', async () => {
|
|
124
|
+
const { client } = createQueryMock({ data: { enabled: false }, error: null });
|
|
125
|
+
mockGetClient.mockResolvedValue(client);
|
|
126
|
+
expect(await shouldEmitTelemetry('user-abc')).toBe(false);
|
|
127
|
+
});
|
|
128
|
+
});
|
|
129
|
+
// ============================================================================
|
|
130
|
+
// (5) Idempotent under concurrent calls — single in-flight query
|
|
131
|
+
// ============================================================================
|
|
132
|
+
describe('resolveConsent — concurrent call deduplication', () => {
|
|
133
|
+
it('issues exactly one Supabase query for two parallel calls on the same id', async () => {
|
|
134
|
+
const { client, maybeSingle } = createQueryMock({ data: { enabled: true }, error: null });
|
|
135
|
+
mockGetClient.mockResolvedValue(client);
|
|
136
|
+
const [s1, s2] = await Promise.all([resolveConsent('user-def'), resolveConsent('user-def')]);
|
|
137
|
+
// Both calls must have resolved to the same consent state.
|
|
138
|
+
expect(s1).toEqual(s2);
|
|
139
|
+
// The DB was only hit once — the cache stored the in-flight Promise.
|
|
140
|
+
expect(maybeSingle).toHaveBeenCalledTimes(1);
|
|
141
|
+
});
|
|
142
|
+
});
|
|
143
|
+
// ============================================================================
|
|
144
|
+
// (6) Fail-safe on Supabase error
|
|
145
|
+
// ============================================================================
|
|
146
|
+
describe('resolveConsent — fail-safe on Supabase error', () => {
|
|
147
|
+
it('returns consent_required: true when maybeSingle rejects', async () => {
|
|
148
|
+
const maybeSingle = vi.fn().mockRejectedValue(new Error('network error'));
|
|
149
|
+
const eq = vi.fn().mockReturnValue({ maybeSingle });
|
|
150
|
+
const select = vi.fn().mockReturnValue({ eq });
|
|
151
|
+
const from = vi.fn().mockReturnValue({ select });
|
|
152
|
+
const client = { from };
|
|
153
|
+
mockGetClient.mockResolvedValue(client);
|
|
154
|
+
const state = await resolveConsent('user-ghi');
|
|
155
|
+
// Fail-safe: consent_required must be true — never silently emit.
|
|
156
|
+
expect(state.consentRequired).toBe(true);
|
|
157
|
+
expect(state.enabled).toBe(false);
|
|
158
|
+
});
|
|
159
|
+
it('returns consent_required: true when query returns an error object', async () => {
|
|
160
|
+
const { client } = createQueryMock({ data: null, error: { message: 'permission denied' } });
|
|
161
|
+
mockGetClient.mockResolvedValue(client);
|
|
162
|
+
const state = await resolveConsent('user-ghi');
|
|
163
|
+
expect(state.consentRequired).toBe(true);
|
|
164
|
+
expect(state.enabled).toBe(false);
|
|
165
|
+
});
|
|
166
|
+
it('shouldEmitTelemetry returns false when Supabase errors', async () => {
|
|
167
|
+
const { client } = createQueryMock({ data: null, error: { message: 'db error' } });
|
|
168
|
+
mockGetClient.mockResolvedValue(client);
|
|
169
|
+
expect(await shouldEmitTelemetry('user-ghi')).toBe(false);
|
|
170
|
+
});
|
|
171
|
+
it('returns no-id state (no prompt) when getSupabaseClient itself throws', async () => {
|
|
172
|
+
// This covers the "Supabase not configured" offline branch.
|
|
173
|
+
mockGetClient.mockRejectedValue(new Error('Supabase not configured'));
|
|
174
|
+
const state = await resolveConsent('user-ghi-offline');
|
|
175
|
+
// Offline: suppress telemetry but do NOT demand consent (no network surface).
|
|
176
|
+
expect(state.enabled).toBe(false);
|
|
177
|
+
// consentRequired is false in the offline/unconfigured branch — matches DEFAULT_NO_ID.
|
|
178
|
+
expect(state.consentRequired).toBe(false);
|
|
179
|
+
});
|
|
180
|
+
});
|
|
181
|
+
// ============================================================================
|
|
182
|
+
// (7) Cache invalidation
|
|
183
|
+
// ============================================================================
|
|
184
|
+
describe('invalidateConsentCache', () => {
|
|
185
|
+
it('causes next resolveConsent call to re-query Supabase after invalidation', async () => {
|
|
186
|
+
const { client, maybeSingle } = createQueryMock({ data: { enabled: true }, error: null });
|
|
187
|
+
mockGetClient.mockResolvedValue(client);
|
|
188
|
+
// First call — populates cache.
|
|
189
|
+
await resolveConsent('user-jkl');
|
|
190
|
+
expect(maybeSingle).toHaveBeenCalledTimes(1);
|
|
191
|
+
// Invalidate.
|
|
192
|
+
invalidateConsentCache('user-jkl');
|
|
193
|
+
// Second call — must re-query.
|
|
194
|
+
await resolveConsent('user-jkl');
|
|
195
|
+
expect(maybeSingle).toHaveBeenCalledTimes(2);
|
|
196
|
+
});
|
|
197
|
+
it('does not affect cache entries for other anonymous_ids', async () => {
|
|
198
|
+
const { client, maybeSingle } = createQueryMock({ data: { enabled: true }, error: null });
|
|
199
|
+
mockGetClient.mockResolvedValue(client);
|
|
200
|
+
await resolveConsent('user-jkl');
|
|
201
|
+
await resolveConsent('user-other');
|
|
202
|
+
expect(maybeSingle).toHaveBeenCalledTimes(2);
|
|
203
|
+
invalidateConsentCache('user-jkl');
|
|
204
|
+
// Only user-jkl re-queries; user-other is still cached.
|
|
205
|
+
await resolveConsent('user-jkl');
|
|
206
|
+
await resolveConsent('user-other');
|
|
207
|
+
expect(maybeSingle).toHaveBeenCalledTimes(3);
|
|
208
|
+
});
|
|
209
|
+
it('clears entire cache when called without argument', async () => {
|
|
210
|
+
const { client, maybeSingle } = createQueryMock({ data: { enabled: true }, error: null });
|
|
211
|
+
mockGetClient.mockResolvedValue(client);
|
|
212
|
+
await resolveConsent('user-a');
|
|
213
|
+
await resolveConsent('user-b');
|
|
214
|
+
expect(maybeSingle).toHaveBeenCalledTimes(2);
|
|
215
|
+
invalidateConsentCache();
|
|
216
|
+
await resolveConsent('user-a');
|
|
217
|
+
await resolveConsent('user-b');
|
|
218
|
+
expect(maybeSingle).toHaveBeenCalledTimes(4);
|
|
219
|
+
});
|
|
220
|
+
});
|
|
221
|
+
// ============================================================================
|
|
222
|
+
// (8–11) annotateResponseWithConsent
|
|
223
|
+
// ============================================================================
|
|
224
|
+
/** Minimal MCP CallToolResult-shaped envelope. */
|
|
225
|
+
function makeEnvelope(text) {
|
|
226
|
+
return { content: [{ type: 'text', text }] };
|
|
227
|
+
}
|
|
228
|
+
const UNRESOLVED_CONSENT = {
|
|
229
|
+
enabled: false,
|
|
230
|
+
consentRequired: true,
|
|
231
|
+
privacyUrl: TELEMETRY_PRIVACY_URL,
|
|
232
|
+
};
|
|
233
|
+
const RESOLVED_CONSENT = {
|
|
234
|
+
enabled: true,
|
|
235
|
+
consentRequired: false,
|
|
236
|
+
privacyUrl: TELEMETRY_PRIVACY_URL,
|
|
237
|
+
};
|
|
238
|
+
describe('annotateResponseWithConsent', () => {
|
|
239
|
+
it('(8) splices consent_required and privacy_url when consent is unresolved', () => {
|
|
240
|
+
const envelope = makeEnvelope(JSON.stringify({ result: 'ok' }));
|
|
241
|
+
const out = annotateResponseWithConsent(envelope, UNRESOLVED_CONSENT);
|
|
242
|
+
const parsed = JSON.parse(out.content[0].text);
|
|
243
|
+
expect(parsed.result).toBe('ok');
|
|
244
|
+
expect(parsed.consent_required).toBe(true);
|
|
245
|
+
expect(parsed.privacy_url).toBe(TELEMETRY_PRIVACY_URL);
|
|
246
|
+
});
|
|
247
|
+
it('(9) returns envelope unchanged when consent is resolved (passthrough)', () => {
|
|
248
|
+
const text = JSON.stringify({ result: 'ok' });
|
|
249
|
+
const envelope = makeEnvelope(text);
|
|
250
|
+
const out = annotateResponseWithConsent(envelope, RESOLVED_CONSENT);
|
|
251
|
+
// Same reference (or at minimum identical content) — nothing added.
|
|
252
|
+
expect(out).toBe(envelope);
|
|
253
|
+
const parsed = JSON.parse(out.content[0].text);
|
|
254
|
+
expect(parsed).not.toHaveProperty('consent_required');
|
|
255
|
+
});
|
|
256
|
+
it('(10) is idempotent — does not re-annotate if fields already present', () => {
|
|
257
|
+
const alreadyAnnotated = { result: 'ok', consent_required: true, privacy_url: 'https://x.y' };
|
|
258
|
+
const envelope = makeEnvelope(JSON.stringify(alreadyAnnotated));
|
|
259
|
+
const out = annotateResponseWithConsent(envelope, UNRESOLVED_CONSENT);
|
|
260
|
+
// Must return the same reference — no mutation of pre-existing annotation.
|
|
261
|
+
expect(out).toBe(envelope);
|
|
262
|
+
const parsed = JSON.parse(out.content[0].text);
|
|
263
|
+
// privacy_url should remain the original value, not overwritten.
|
|
264
|
+
expect(parsed.privacy_url).toBe('https://x.y');
|
|
265
|
+
});
|
|
266
|
+
it('(11) returns envelope unchanged when text is malformed JSON (no throw)', () => {
|
|
267
|
+
const envelope = makeEnvelope('not-valid-json{{');
|
|
268
|
+
expect(() => annotateResponseWithConsent(envelope, UNRESOLVED_CONSENT)).not.toThrow();
|
|
269
|
+
const out = annotateResponseWithConsent(envelope, UNRESOLVED_CONSENT);
|
|
270
|
+
expect(out).toBe(envelope);
|
|
271
|
+
});
|
|
272
|
+
it('returns envelope unchanged when content array is empty', () => {
|
|
273
|
+
const envelope = { content: [] };
|
|
274
|
+
const out = annotateResponseWithConsent(envelope, UNRESOLVED_CONSENT);
|
|
275
|
+
expect(out).toBe(envelope);
|
|
276
|
+
});
|
|
277
|
+
it('returns envelope unchanged when first content item is not type=text', () => {
|
|
278
|
+
const envelope = { content: [{ type: 'image', url: 'https://example.com/img.png' }] };
|
|
279
|
+
const out = annotateResponseWithConsent(envelope, UNRESOLVED_CONSENT);
|
|
280
|
+
expect(out).toBe(envelope);
|
|
281
|
+
});
|
|
282
|
+
});
|
|
283
|
+
// ============================================================================
|
|
284
|
+
// (12) Privacy URL literal
|
|
285
|
+
// ============================================================================
|
|
286
|
+
describe('TELEMETRY_PRIVACY_URL', () => {
|
|
287
|
+
it('equals the canonical consent dashboard URL', () => {
|
|
288
|
+
expect(TELEMETRY_PRIVACY_URL).toBe('https://skillsmith.app/account/telemetry');
|
|
289
|
+
});
|
|
290
|
+
});
|
|
291
|
+
//# sourceMappingURL=telemetry-consent.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"telemetry-consent.test.js","sourceRoot":"","sources":["../../../src/middleware/telemetry-consent.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SMI-5018 W2.S3 — MCP-tree telemetry coverage snapshot test.
|
|
3
|
+
*
|
|
4
|
+
* Scope: `packages/mcp-server/src/tools/` only (v1).
|
|
5
|
+
* CLI + VS Code trees are NOT checked here — they are blocked by SMI-5040
|
|
6
|
+
* (anonymous-closure incompatibility). When SMI-5040 lands this test will
|
|
7
|
+
* be extended to cover those trees.
|
|
8
|
+
*
|
|
9
|
+
* Risk guarded (plan line 798, risk #8):
|
|
10
|
+
* "A new dispatcher ships without a telemetry wrap."
|
|
11
|
+
*
|
|
12
|
+
* Strategy: explicit allowlist (40 entries) cross-checked against the live
|
|
13
|
+
* withTelemetry import-site count. Allowlist chosen over heuristic-walk
|
|
14
|
+
* because it is trivially auditable — each entry maps 1-to-1 to a
|
|
15
|
+
* `grep "= withTelemetry"` result, and the SOURCE_FILE_COUNT sentinel
|
|
16
|
+
* independently guards against drift in either direction.
|
|
17
|
+
*
|
|
18
|
+
* When you add a new dispatcher:
|
|
19
|
+
* 1. Wrap it with withTelemetry in its source file (as SMI-5017 did).
|
|
20
|
+
* 2. Add its export name to EXPECTED_DISPATCHERS below.
|
|
21
|
+
* 3. Update SOURCE_FILE_COUNT if the dispatcher lives in a new file.
|
|
22
|
+
* The test will fail in CI until all three steps are done.
|
|
23
|
+
*/
|
|
24
|
+
export {};
|
|
25
|
+
//# sourceMappingURL=telemetry-coverage.test.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"telemetry-coverage.test.d.ts","sourceRoot":"","sources":["../../../../src/tools/__meta__/telemetry-coverage.test.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG"}
|