@askthew/mcp-plugin 0.4.6 → 0.4.7
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/dist/index.js +29 -1
- package/dist/lib/telemetry.js +1 -1
- package/package.json +4 -2
- package/dist/cli-actions.test.d.ts +0 -1
- package/dist/cli-actions.test.js +0 -71
- package/dist/cli.test.d.ts +0 -1
- package/dist/cli.test.js +0 -323
- package/dist/free-tier-policy.test.d.ts +0 -1
- package/dist/free-tier-policy.test.js +0 -58
- package/dist/index.test.d.ts +0 -1
- package/dist/index.test.js +0 -950
- package/dist/install.test.d.ts +0 -1
- package/dist/install.test.js +0 -314
- package/dist/local-identity.test.d.ts +0 -1
- package/dist/local-identity.test.js +0 -29
- package/dist/local-store.test.d.ts +0 -1
- package/dist/local-store.test.js +0 -71
- package/dist/scope.test.d.ts +0 -1
- package/dist/scope.test.js +0 -49
- package/dist/timeline-insights.test.d.ts +0 -1
- package/dist/timeline-insights.test.js +0 -85
- package/dist/tip-engine.test.d.ts +0 -1
- package/dist/tip-engine.test.js +0 -51
package/dist/index.test.js
DELETED
|
@@ -1,950 +0,0 @@
|
|
|
1
|
-
import test from "node:test";
|
|
2
|
-
import assert from "node:assert/strict";
|
|
3
|
-
import fs from "node:fs";
|
|
4
|
-
import os from "node:os";
|
|
5
|
-
import path from "node:path";
|
|
6
|
-
import { codingSessionSignalSchema, createAskTheWMcpServer, normalizeInstallTokenInput, redactCodingSessionSignal, redactProvenanceSignal, } from "./index.js";
|
|
7
|
-
import { LocalStore } from "./lib/local-store.js";
|
|
8
|
-
import { ensureLocalIdentity } from "./lib/local-identity.js";
|
|
9
|
-
function toolResultJson(result) {
|
|
10
|
-
return JSON.parse(result.content[0].text);
|
|
11
|
-
}
|
|
12
|
-
async function withFreeEnv(fn) {
|
|
13
|
-
const previous = {
|
|
14
|
-
ASKTHEW_CLI_TOKEN: process.env.ASKTHEW_CLI_TOKEN,
|
|
15
|
-
ASKTHEW_USER_ID: process.env.ASKTHEW_USER_ID,
|
|
16
|
-
ASKTHEW_CLI_TOKEN_ID: process.env.ASKTHEW_CLI_TOKEN_ID,
|
|
17
|
-
ASKTHEW_DATA_DIR: process.env.ASKTHEW_DATA_DIR,
|
|
18
|
-
ASKTHEW_INSTALL_TOKEN: process.env.ASKTHEW_INSTALL_TOKEN,
|
|
19
|
-
ASKTHEW_FREE_MODE: process.env.ASKTHEW_FREE_MODE,
|
|
20
|
-
};
|
|
21
|
-
const dataDir = fs.mkdtempSync(path.join(os.tmpdir(), "askthew-free-tools-"));
|
|
22
|
-
process.env.ASKTHEW_DATA_DIR = dataDir;
|
|
23
|
-
ensureLocalIdentity({ emailClaim: "founder@example.com" });
|
|
24
|
-
delete process.env.ASKTHEW_CLI_TOKEN;
|
|
25
|
-
delete process.env.ASKTHEW_USER_ID;
|
|
26
|
-
delete process.env.ASKTHEW_CLI_TOKEN_ID;
|
|
27
|
-
delete process.env.ASKTHEW_INSTALL_TOKEN;
|
|
28
|
-
delete process.env.ASKTHEW_FREE_MODE;
|
|
29
|
-
try {
|
|
30
|
-
return await fn();
|
|
31
|
-
}
|
|
32
|
-
finally {
|
|
33
|
-
for (const [key, value] of Object.entries(previous)) {
|
|
34
|
-
if (value === undefined) {
|
|
35
|
-
delete process.env[key];
|
|
36
|
-
}
|
|
37
|
-
else {
|
|
38
|
-
process.env[key] = value;
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
fs.rmSync(dataDir, { recursive: true, force: true });
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
async function withPendingFreeEnv(fn) {
|
|
45
|
-
const previous = {
|
|
46
|
-
ASKTHEW_CLI_TOKEN: process.env.ASKTHEW_CLI_TOKEN,
|
|
47
|
-
ASKTHEW_USER_ID: process.env.ASKTHEW_USER_ID,
|
|
48
|
-
ASKTHEW_CLI_TOKEN_ID: process.env.ASKTHEW_CLI_TOKEN_ID,
|
|
49
|
-
ASKTHEW_DATA_DIR: process.env.ASKTHEW_DATA_DIR,
|
|
50
|
-
ASKTHEW_INSTALL_TOKEN: process.env.ASKTHEW_INSTALL_TOKEN,
|
|
51
|
-
ASKTHEW_FREE_MODE: process.env.ASKTHEW_FREE_MODE,
|
|
52
|
-
};
|
|
53
|
-
const dataDir = fs.mkdtempSync(path.join(os.tmpdir(), "askthew-pending-free-tools-"));
|
|
54
|
-
process.env.ASKTHEW_FREE_MODE = "1";
|
|
55
|
-
process.env.ASKTHEW_DATA_DIR = dataDir;
|
|
56
|
-
delete process.env.ASKTHEW_CLI_TOKEN;
|
|
57
|
-
delete process.env.ASKTHEW_USER_ID;
|
|
58
|
-
delete process.env.ASKTHEW_CLI_TOKEN_ID;
|
|
59
|
-
delete process.env.ASKTHEW_INSTALL_TOKEN;
|
|
60
|
-
try {
|
|
61
|
-
return await fn();
|
|
62
|
-
}
|
|
63
|
-
finally {
|
|
64
|
-
for (const [key, value] of Object.entries(previous)) {
|
|
65
|
-
if (value === undefined) {
|
|
66
|
-
delete process.env[key];
|
|
67
|
-
}
|
|
68
|
-
else {
|
|
69
|
-
process.env[key] = value;
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
fs.rmSync(dataDir, { recursive: true, force: true });
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
async function withInstalledFreeEnv(fn) {
|
|
76
|
-
const previous = {
|
|
77
|
-
ASKTHEW_CLI_TOKEN: process.env.ASKTHEW_CLI_TOKEN,
|
|
78
|
-
ASKTHEW_USER_ID: process.env.ASKTHEW_USER_ID,
|
|
79
|
-
ASKTHEW_CLI_TOKEN_ID: process.env.ASKTHEW_CLI_TOKEN_ID,
|
|
80
|
-
ASKTHEW_DATA_DIR: process.env.ASKTHEW_DATA_DIR,
|
|
81
|
-
ASKTHEW_INSTALL_TOKEN: process.env.ASKTHEW_INSTALL_TOKEN,
|
|
82
|
-
ASKTHEW_FREE_MODE: process.env.ASKTHEW_FREE_MODE,
|
|
83
|
-
};
|
|
84
|
-
const dataDir = fs.mkdtempSync(path.join(os.tmpdir(), "askthew-installed-free-tools-"));
|
|
85
|
-
process.env.ASKTHEW_FREE_MODE = "1";
|
|
86
|
-
process.env.ASKTHEW_DATA_DIR = dataDir;
|
|
87
|
-
ensureLocalIdentity({ emailClaim: "ymtest89+test5@gmail.com" });
|
|
88
|
-
delete process.env.ASKTHEW_CLI_TOKEN;
|
|
89
|
-
delete process.env.ASKTHEW_USER_ID;
|
|
90
|
-
delete process.env.ASKTHEW_CLI_TOKEN_ID;
|
|
91
|
-
delete process.env.ASKTHEW_INSTALL_TOKEN;
|
|
92
|
-
try {
|
|
93
|
-
return await fn();
|
|
94
|
-
}
|
|
95
|
-
finally {
|
|
96
|
-
for (const [key, value] of Object.entries(previous)) {
|
|
97
|
-
if (value === undefined) {
|
|
98
|
-
delete process.env[key];
|
|
99
|
-
}
|
|
100
|
-
else {
|
|
101
|
-
process.env[key] = value;
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
fs.rmSync(dataDir, { recursive: true, force: true });
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
test("install token normalization accepts copied shell-quoted values", () => {
|
|
108
|
-
assert.equal(normalizeInstallTokenInput("'atw_mcp_token'"), "atw_mcp_token");
|
|
109
|
-
assert.equal(normalizeInstallTokenInput('"atw_mcp_token"'), "atw_mcp_token");
|
|
110
|
-
assert.equal(normalizeInstallTokenInput(" atw_mcp_token "), "atw_mcp_token");
|
|
111
|
-
});
|
|
112
|
-
test("coding session signal schema accepts compact checkpoints", () => {
|
|
113
|
-
const parsed = codingSessionSignalSchema.parse({
|
|
114
|
-
sessionId: "session-1",
|
|
115
|
-
sequence: 2,
|
|
116
|
-
kind: "direction_change",
|
|
117
|
-
summary: "User chose app-level inference and capture-only connector behavior.",
|
|
118
|
-
evidence: [{ role: "user", excerpt: "the app does many of it" }],
|
|
119
|
-
filesTouched: ["packages/mcp-plugin/src/index.ts"],
|
|
120
|
-
commandsRun: ["npm test --workspace @askthew/mcp-plugin"],
|
|
121
|
-
metadata: { hostType: "codex" },
|
|
122
|
-
});
|
|
123
|
-
assert.equal(parsed.kind, "direction_change");
|
|
124
|
-
assert.equal(parsed.evidence[0]?.role, "user");
|
|
125
|
-
});
|
|
126
|
-
test("coding session signal redaction removes obvious secrets", () => {
|
|
127
|
-
const redacted = redactCodingSessionSignal({
|
|
128
|
-
sessionId: "session-1",
|
|
129
|
-
sequence: 3,
|
|
130
|
-
kind: "verification_result",
|
|
131
|
-
summary: "Ran command with sk_live_1234567890abcdefghijklmnop",
|
|
132
|
-
evidence: [{ role: "assistant", excerpt: "Token eyJabcdefghijklmnopqrstuv.abcdefghijklmnopqrstuv.abcdefghijklmnopqrstuv" }],
|
|
133
|
-
filesTouched: [],
|
|
134
|
-
commandsRun: ["curl -H 'Authorization: Bearer AKIA1234567890ABCDEF'"],
|
|
135
|
-
metadata: { nested: { token: "sk_test_1234567890abcdefghijklmnop" } },
|
|
136
|
-
});
|
|
137
|
-
assert.match(redacted.summary, /\[REDACTED\]/);
|
|
138
|
-
assert.match(redacted.evidence[0]?.excerpt ?? "", /\[REDACTED\]/);
|
|
139
|
-
assert.match(redacted.commandsRun[0] ?? "", /\[REDACTED\]/);
|
|
140
|
-
assert.deepEqual(redacted.metadata, { nested: { token: "[REDACTED]" } });
|
|
141
|
-
});
|
|
142
|
-
test("capture redactor catches documented capture-path patterns", () => {
|
|
143
|
-
const redacted = redactCodingSessionSignal({
|
|
144
|
-
sessionId: "session-1",
|
|
145
|
-
sequence: 11,
|
|
146
|
-
kind: "session_checkpoint",
|
|
147
|
-
summary: "OPENAI_API_KEY=sk-proj_abcdefghijklmnopqrstuvwxyz123456 Bearer abc.def-ghi user@example.com AKIA1234567890ABCDEF eyJabcdefghijklmnopqrstuv.abcdefghijklmnopqrstuv.abcdefghijklmnopqrstuv",
|
|
148
|
-
evidence: [],
|
|
149
|
-
filesTouched: [],
|
|
150
|
-
commandsRun: [],
|
|
151
|
-
metadata: {},
|
|
152
|
-
});
|
|
153
|
-
assert.equal(redacted.summary, "[REDACTED] [REDACTED] [REDACTED] [REDACTED] [REDACTED]");
|
|
154
|
-
});
|
|
155
|
-
test("redacts ATW hyphen-segmented install tokens in commands", () => {
|
|
156
|
-
const redacted = redactCodingSessionSignal({
|
|
157
|
-
sessionId: "session-1",
|
|
158
|
-
sequence: 4,
|
|
159
|
-
kind: "verification_result",
|
|
160
|
-
summary: "Installed the connector.",
|
|
161
|
-
evidence: [],
|
|
162
|
-
filesTouched: [],
|
|
163
|
-
commandsRun: [
|
|
164
|
-
"npx @askthew/mcp-plugin install --token atw_mcp_aed8621b-1456-4e63-833b-3c98109b6f18_f56c5683abc123ff --host codex",
|
|
165
|
-
],
|
|
166
|
-
metadata: {},
|
|
167
|
-
});
|
|
168
|
-
assert.equal(redacted.commandsRun[0], "npx @askthew/mcp-plugin install --token [REDACTED] --host codex");
|
|
169
|
-
});
|
|
170
|
-
test("strips URL credentials and sensitive query parameters while preserving URL shape", () => {
|
|
171
|
-
const redacted = redactCodingSessionSignal({
|
|
172
|
-
sessionId: "session-1",
|
|
173
|
-
sequence: 5,
|
|
174
|
-
kind: "session_checkpoint",
|
|
175
|
-
summary: "Checked https://admin:pass@db.example.com/app?token=secret123&view=signals",
|
|
176
|
-
evidence: [],
|
|
177
|
-
filesTouched: [],
|
|
178
|
-
commandsRun: [],
|
|
179
|
-
metadata: {},
|
|
180
|
-
});
|
|
181
|
-
assert.equal(redacted.summary, "Checked https://[REDACTED]:[REDACTED]@db.example.com/app?token=[REDACTED]&view=signals");
|
|
182
|
-
});
|
|
183
|
-
test("sanitizes sensitive file paths without touching normal repo paths", () => {
|
|
184
|
-
const redacted = redactCodingSessionSignal({
|
|
185
|
-
sessionId: "session-1",
|
|
186
|
-
sequence: 6,
|
|
187
|
-
kind: "implementation_update",
|
|
188
|
-
summary: "Updated auth routes.",
|
|
189
|
-
evidence: [],
|
|
190
|
-
filesTouched: ["/Users/alice/.ssh/id_rsa", "apps/app/server/auth.ts"],
|
|
191
|
-
commandsRun: [],
|
|
192
|
-
metadata: {},
|
|
193
|
-
});
|
|
194
|
-
assert.deepEqual(redacted.filesTouched, ["[SENSITIVE_PATH]/id_rsa", "apps/app/server/auth.ts"]);
|
|
195
|
-
});
|
|
196
|
-
test("redacts CLI secret flags and preserves non-sensitive flags", () => {
|
|
197
|
-
const redacted = redactCodingSessionSignal({
|
|
198
|
-
sessionId: "session-1",
|
|
199
|
-
sequence: 7,
|
|
200
|
-
kind: "verification_result",
|
|
201
|
-
summary: "Ran setup.",
|
|
202
|
-
evidence: [],
|
|
203
|
-
filesTouched: [],
|
|
204
|
-
commandsRun: ["askthew-mcp install --token supersecret123abc --host codex --password \"secret-value\""],
|
|
205
|
-
metadata: {},
|
|
206
|
-
});
|
|
207
|
-
assert.equal(redacted.commandsRun[0], "askthew-mcp install --token [REDACTED] --host codex --password \"[REDACTED]\"");
|
|
208
|
-
});
|
|
209
|
-
test("redacts high entropy evidence and metadata without applying entropy to summary", () => {
|
|
210
|
-
const generatedSecret = "aZ9xP0vQ1rS2tU3wX4yZ5aB6cD7eF8gH9";
|
|
211
|
-
const redacted = redactCodingSessionSignal({
|
|
212
|
-
sessionId: "session-1",
|
|
213
|
-
sequence: 8,
|
|
214
|
-
kind: "session_checkpoint",
|
|
215
|
-
summary: `Architecture note ${generatedSecret}`,
|
|
216
|
-
evidence: [{ role: "assistant", excerpt: `Saw token ${generatedSecret}` }],
|
|
217
|
-
filesTouched: [],
|
|
218
|
-
commandsRun: [`curl --header X-Test:${generatedSecret}`],
|
|
219
|
-
metadata: { sample: generatedSecret },
|
|
220
|
-
});
|
|
221
|
-
assert.equal(redacted.summary, `Architecture note ${generatedSecret}`);
|
|
222
|
-
assert.equal(redacted.evidence[0]?.excerpt, "Saw token [REDACTED]");
|
|
223
|
-
assert.equal(redacted.commandsRun[0], "curl --header X-Test:[REDACTED]");
|
|
224
|
-
assert.deepEqual(redacted.metadata, { sample: "[REDACTED]" });
|
|
225
|
-
});
|
|
226
|
-
test("redacts PII and payment identifiers", () => {
|
|
227
|
-
const redacted = redactCodingSessionSignal({
|
|
228
|
-
sessionId: "session-1",
|
|
229
|
-
sequence: 9,
|
|
230
|
-
kind: "session_checkpoint",
|
|
231
|
-
summary: "Contact patient@hospital.org, SSN 123-45-6789, phone 415-555-1212, card 4242424242424242.",
|
|
232
|
-
evidence: [],
|
|
233
|
-
filesTouched: [],
|
|
234
|
-
commandsRun: [],
|
|
235
|
-
metadata: {},
|
|
236
|
-
});
|
|
237
|
-
assert.equal(redacted.summary, "Contact [REDACTED], SSN [REDACTED], phone [REDACTED], card [REDACTED].");
|
|
238
|
-
});
|
|
239
|
-
test("preserves raw decision-quality signal language and normal paths", () => {
|
|
240
|
-
const summary = [
|
|
241
|
-
"User approved switching from REST to GraphQL.",
|
|
242
|
-
"User rejected Redis caching approach.",
|
|
243
|
-
"Architecture decision: monorepo over multi-repo.",
|
|
244
|
-
].join(" ");
|
|
245
|
-
const redacted = redactCodingSessionSignal({
|
|
246
|
-
sessionId: "session-1",
|
|
247
|
-
sequence: 10,
|
|
248
|
-
kind: "direction_change",
|
|
249
|
-
summary,
|
|
250
|
-
evidence: [],
|
|
251
|
-
filesTouched: ["apps/app/server/auth.ts"],
|
|
252
|
-
commandsRun: ["askthew-mcp install --host codex"],
|
|
253
|
-
metadata: {},
|
|
254
|
-
});
|
|
255
|
-
assert.equal(redacted.summary, summary);
|
|
256
|
-
assert.deepEqual(redacted.filesTouched, ["apps/app/server/auth.ts"]);
|
|
257
|
-
assert.deepEqual(redacted.commandsRun, ["askthew-mcp install --host codex"]);
|
|
258
|
-
});
|
|
259
|
-
test("provenance metadata is recursively redacted", () => {
|
|
260
|
-
const redacted = redactProvenanceSignal({
|
|
261
|
-
source: "mcp_plugin",
|
|
262
|
-
decision: "Keep raw signal capture.",
|
|
263
|
-
rationale: "Decision V5 owns extraction.",
|
|
264
|
-
confidence: 0.9,
|
|
265
|
-
filesAffected: ["/Users/alice/.env"],
|
|
266
|
-
sessionId: "session-1",
|
|
267
|
-
metadata: {
|
|
268
|
-
nested: {
|
|
269
|
-
url: "https://admin:secret@example.com/path?api_key=secret123",
|
|
270
|
-
},
|
|
271
|
-
},
|
|
272
|
-
});
|
|
273
|
-
assert.deepEqual(redacted.filesAffected, ["[SENSITIVE_PATH]/.env"]);
|
|
274
|
-
assert.equal(redacted.metadata.nested.url, "https://[REDACTED]:[REDACTED]@example.com/path?api_key=[REDACTED]");
|
|
275
|
-
});
|
|
276
|
-
test("happy-path MCP source exposes capture_session_signal and v1 API tools", () => {
|
|
277
|
-
const source = fs.readFileSync(path.resolve(process.cwd(), "src/index.ts"), "utf8");
|
|
278
|
-
for (const toolName of [
|
|
279
|
-
"capture_session_signal",
|
|
280
|
-
"list_decisions",
|
|
281
|
-
"get_decision",
|
|
282
|
-
"create_decision",
|
|
283
|
-
"update_decision",
|
|
284
|
-
"delete_decision",
|
|
285
|
-
"review_decisions",
|
|
286
|
-
"review_session",
|
|
287
|
-
"recap",
|
|
288
|
-
"coach",
|
|
289
|
-
"promote_signal_to_decision",
|
|
290
|
-
"list_decision_candidates",
|
|
291
|
-
"search_trail",
|
|
292
|
-
"list_outcomes",
|
|
293
|
-
"get_outcome",
|
|
294
|
-
"list_outcome_signals",
|
|
295
|
-
"create_outcome",
|
|
296
|
-
"update_outcome",
|
|
297
|
-
"delete_outcome",
|
|
298
|
-
"get_north_star",
|
|
299
|
-
"update_north_star",
|
|
300
|
-
"list_signals",
|
|
301
|
-
"get_signal",
|
|
302
|
-
"find_signal_by_summary",
|
|
303
|
-
]) {
|
|
304
|
-
assert.match(source, new RegExp(`"${toolName}"`));
|
|
305
|
-
}
|
|
306
|
-
assert.doesNotMatch(source, /server\.tool\(\s*"verify_install"/);
|
|
307
|
-
assert.doesNotMatch(source, /server\.tool\(\s*"record_decision"/);
|
|
308
|
-
assert.doesNotMatch(source, /server\.tool\(\s*"infer_decisions"/);
|
|
309
|
-
assert.doesNotMatch(source, /server\.tool\(\s*"get_session_decisions"/);
|
|
310
|
-
assert.doesNotMatch(source, /server\.tool\(\s*"link_outcome"/);
|
|
311
|
-
assert.doesNotMatch(source, /server\.tool\(\s*"get_decision_feed"/);
|
|
312
|
-
assert.doesNotMatch(source, /server\.tool\(\s*"analyze_session"/);
|
|
313
|
-
});
|
|
314
|
-
test("schema-handler contract registers every documented MCP tool", async () => {
|
|
315
|
-
await withFreeEnv(async () => {
|
|
316
|
-
const server = createAskTheWMcpServer({ sendStartupHeartbeat: false });
|
|
317
|
-
const tools = server._registeredTools;
|
|
318
|
-
const expected = [
|
|
319
|
-
"capture_session_signal",
|
|
320
|
-
"list_signals",
|
|
321
|
-
"get_signal",
|
|
322
|
-
"find_signal_by_summary",
|
|
323
|
-
"list_decisions",
|
|
324
|
-
"get_decision",
|
|
325
|
-
"create_decision",
|
|
326
|
-
"update_decision",
|
|
327
|
-
"delete_decision",
|
|
328
|
-
"review_decisions",
|
|
329
|
-
"review_session",
|
|
330
|
-
"view_timeline",
|
|
331
|
-
"recap",
|
|
332
|
-
"coach",
|
|
333
|
-
"promote_signal_to_decision",
|
|
334
|
-
"list_decision_candidates",
|
|
335
|
-
"search_trail",
|
|
336
|
-
"list_outcomes",
|
|
337
|
-
"get_outcome",
|
|
338
|
-
"list_outcome_signals",
|
|
339
|
-
"create_outcome",
|
|
340
|
-
"update_outcome",
|
|
341
|
-
"delete_outcome",
|
|
342
|
-
"get_north_star",
|
|
343
|
-
"update_north_star",
|
|
344
|
-
"export_decisions",
|
|
345
|
-
];
|
|
346
|
-
for (const toolName of expected) {
|
|
347
|
-
assert.equal(typeof tools[toolName]?.handler, "function", `${toolName} handler`);
|
|
348
|
-
assert.ok(tools[toolName]?.inputSchema, `${toolName} schema`);
|
|
349
|
-
}
|
|
350
|
-
});
|
|
351
|
-
});
|
|
352
|
-
test("every write tool accepts an idempotency key parameter", () => {
|
|
353
|
-
const source = fs.readFileSync(path.resolve(process.cwd(), "src/index.ts"), "utf8");
|
|
354
|
-
for (const toolName of [
|
|
355
|
-
"capture_session_signal",
|
|
356
|
-
"create_decision",
|
|
357
|
-
"update_decision",
|
|
358
|
-
"delete_decision",
|
|
359
|
-
"create_outcome",
|
|
360
|
-
"update_outcome",
|
|
361
|
-
"delete_outcome",
|
|
362
|
-
"update_north_star",
|
|
363
|
-
"promote_signal_to_decision",
|
|
364
|
-
]) {
|
|
365
|
-
const start = source.indexOf(`"${toolName}"`);
|
|
366
|
-
assert.notEqual(start, -1, `${toolName} registered`);
|
|
367
|
-
const nextTool = source.indexOf("server.tool(", start + 1);
|
|
368
|
-
const block = source.slice(start, nextTool === -1 ? undefined : nextTool);
|
|
369
|
-
assert.match(block, /idempotencyKey/, `${toolName} idempotencyKey`);
|
|
370
|
-
}
|
|
371
|
-
});
|
|
372
|
-
test("v1 API MCP tools dispatch to expected HTTP routes with install-token auth", async () => {
|
|
373
|
-
const calls = [];
|
|
374
|
-
const server = createAskTheWMcpServer({
|
|
375
|
-
apiBaseUrl: "https://askthew.example.com",
|
|
376
|
-
sendStartupHeartbeat: false,
|
|
377
|
-
credentials: {
|
|
378
|
-
installToken: "atw_mcp_tools",
|
|
379
|
-
clientId: "codex",
|
|
380
|
-
clientLabel: "Codex",
|
|
381
|
-
serverName: "askthew",
|
|
382
|
-
},
|
|
383
|
-
fetchImpl: (async (url, init) => {
|
|
384
|
-
calls.push({
|
|
385
|
-
url: String(url),
|
|
386
|
-
method: String(init?.method ?? "GET"),
|
|
387
|
-
headers: init?.headers,
|
|
388
|
-
body: init?.body ? JSON.parse(String(init.body)) : undefined,
|
|
389
|
-
});
|
|
390
|
-
return new Response(JSON.stringify({ ok: true, route: String(url), method: init?.method }), {
|
|
391
|
-
status: 200,
|
|
392
|
-
});
|
|
393
|
-
}),
|
|
394
|
-
});
|
|
395
|
-
const tools = server._registeredTools;
|
|
396
|
-
const cases = [
|
|
397
|
-
{ name: "list_decisions", payload: { limit: 5, cursor: "c1" }, method: "GET", path: "/api/decisions?limit=5&cursor=c1&compact=true&max_chars=8000" },
|
|
398
|
-
{ name: "get_decision", payload: { id: "d1" }, method: "GET", path: "/api/decisions/d1" },
|
|
399
|
-
{ name: "create_decision", payload: { content: "Adopt Bun", idempotencyKey: "idem-create" }, method: "POST", path: "/api/decisions?response_shape=v2" },
|
|
400
|
-
{ name: "update_decision", payload: { id: "d1", headline: "Adopt Bun v2", idempotencyKey: "idem-update" }, method: "PATCH", path: "/api/decisions/d1?response_shape=v2" },
|
|
401
|
-
{ name: "delete_decision", payload: { id: "d1", confirmText: "Adopt Bun v2" }, method: "DELETE", path: "/api/decisions/d1" },
|
|
402
|
-
{ name: "list_outcomes", payload: { limit: 10 }, method: "GET", path: "/api/outcomes?limit=10" },
|
|
403
|
-
{ name: "get_outcome", payload: { id: "o1" }, method: "GET", path: "/api/outcomes/o1" },
|
|
404
|
-
{ name: "list_outcome_signals", payload: { id: "o1" }, method: "GET", path: "/api/outcomes/o1/signals" },
|
|
405
|
-
{ name: "create_outcome", payload: { name: "Reduce churn" }, method: "POST", path: "/api/outcomes" },
|
|
406
|
-
{ name: "update_outcome", payload: { id: "o1", summary: "New summary", idempotencyKey: "idem-outcome" }, method: "PATCH", path: "/api/outcomes/o1?response_shape=v2" },
|
|
407
|
-
{ name: "delete_outcome", payload: { id: "o1", confirmText: "Reduce churn" }, method: "DELETE", path: "/api/outcomes/o1" },
|
|
408
|
-
{ name: "get_north_star", payload: {}, method: "GET", path: "/api/north-star" },
|
|
409
|
-
{ name: "update_north_star", payload: { metric: "Active users", current: "10", target: "100", reason: "API smoke" }, method: "POST", path: "/api/north-star" },
|
|
410
|
-
{ name: "list_signals", payload: { limit: 25, cursor: "2026-01-01T00:00:00.000Z" }, method: "GET", path: "/api/signals?limit=25&cursor=2026-01-01T00%3A00%3A00.000Z&compact=true&max_chars=8000" },
|
|
411
|
-
{ name: "get_signal", payload: { id: "s1" }, method: "GET", path: "/api/signals/s1" },
|
|
412
|
-
{ name: "find_signal_by_summary", payload: { query: "adopt", limit: 5 }, method: "GET", path: "/api/signals?query=adopt&limit=5&compact=true&max_chars=8000" },
|
|
413
|
-
];
|
|
414
|
-
for (const entry of cases) {
|
|
415
|
-
assert.ok(tools[entry.name], `${entry.name} should be registered`);
|
|
416
|
-
await tools[entry.name].handler(entry.payload);
|
|
417
|
-
}
|
|
418
|
-
assert.equal(calls.length, cases.length);
|
|
419
|
-
cases.forEach((entry, index) => {
|
|
420
|
-
const call = calls[index];
|
|
421
|
-
assert.equal(call.method, entry.method);
|
|
422
|
-
assert.equal(call.url, `https://askthew.example.com${entry.path}`);
|
|
423
|
-
assert.deepEqual(call.headers, {
|
|
424
|
-
...(entry.method === "GET" ? {} : { "Content-Type": "application/json" }),
|
|
425
|
-
...(entry.payload.idempotencyKey ? { "Idempotency-Key": entry.payload.idempotencyKey } : {}),
|
|
426
|
-
Authorization: "Bearer atw_mcp_tools",
|
|
427
|
-
});
|
|
428
|
-
if (entry.method !== "GET") {
|
|
429
|
-
assert.equal(call.body.installToken, "atw_mcp_tools");
|
|
430
|
-
assert.equal(call.body.clientId, "codex");
|
|
431
|
-
}
|
|
432
|
-
});
|
|
433
|
-
});
|
|
434
|
-
test("v1 API MCP tools return server errors as JSON text", async () => {
|
|
435
|
-
const server = createAskTheWMcpServer({
|
|
436
|
-
apiBaseUrl: "https://askthew.example.com",
|
|
437
|
-
sendStartupHeartbeat: false,
|
|
438
|
-
credentials: {
|
|
439
|
-
installToken: "atw_mcp_tools",
|
|
440
|
-
clientId: "codex",
|
|
441
|
-
},
|
|
442
|
-
fetchImpl: (async () => new Response(JSON.stringify({ error: "Nope", code: "nope" }), {
|
|
443
|
-
status: 500,
|
|
444
|
-
})),
|
|
445
|
-
});
|
|
446
|
-
const result = await server._registeredTools.get_signal.handler({ id: "s1" });
|
|
447
|
-
const parsed = JSON.parse(result.content[0].text);
|
|
448
|
-
assert.equal(parsed.ok, false);
|
|
449
|
-
assert.equal(parsed.status, 500);
|
|
450
|
-
assert.equal(parsed.code, "nope");
|
|
451
|
-
});
|
|
452
|
-
test("compact write tools relay upstream errors instead of pretending success", async () => {
|
|
453
|
-
const server = createAskTheWMcpServer({
|
|
454
|
-
apiBaseUrl: "https://askthew.example.com",
|
|
455
|
-
sendStartupHeartbeat: false,
|
|
456
|
-
credentials: {
|
|
457
|
-
installToken: "atw_mcp_tools",
|
|
458
|
-
clientId: "codex",
|
|
459
|
-
},
|
|
460
|
-
fetchImpl: (async () => new Response(JSON.stringify({ ok: false, code: "invalid_input", field: "content", hint: "Provide content." }), {
|
|
461
|
-
status: 422,
|
|
462
|
-
})),
|
|
463
|
-
});
|
|
464
|
-
const result = await server._registeredTools.create_decision.handler({ content: " " });
|
|
465
|
-
const parsed = toolResultJson(result);
|
|
466
|
-
assert.equal(parsed.ok, false);
|
|
467
|
-
assert.equal(parsed.status, 422);
|
|
468
|
-
assert.equal(parsed.code, "invalid_input");
|
|
469
|
-
assert.equal(parsed.field, "content");
|
|
470
|
-
});
|
|
471
|
-
test("createAskTheWMcpServer sends startup heartbeat and automated setup signal", async () => {
|
|
472
|
-
const calls = [];
|
|
473
|
-
createAskTheWMcpServer({
|
|
474
|
-
apiBaseUrl: "https://askthew.example.com",
|
|
475
|
-
credentials: {
|
|
476
|
-
installToken: "atw_mcp_startup",
|
|
477
|
-
clientId: "codex",
|
|
478
|
-
clientLabel: "Codex",
|
|
479
|
-
serverName: "askthew",
|
|
480
|
-
hostType: "codex",
|
|
481
|
-
},
|
|
482
|
-
runtimeMetadata: {
|
|
483
|
-
test_scope: "startup",
|
|
484
|
-
},
|
|
485
|
-
fetchImpl: (async (url, init) => {
|
|
486
|
-
calls.push({
|
|
487
|
-
url: String(url),
|
|
488
|
-
body: JSON.parse(String(init?.body ?? "{}")),
|
|
489
|
-
});
|
|
490
|
-
return new Response(JSON.stringify({ ok: true }), { status: 200 });
|
|
491
|
-
}),
|
|
492
|
-
});
|
|
493
|
-
for (let attempt = 0; attempt < 20 && calls.length < 2; attempt += 1) {
|
|
494
|
-
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
495
|
-
}
|
|
496
|
-
assert.equal(calls.length, 2);
|
|
497
|
-
assert.equal(calls[0]?.url, "https://askthew.example.com/api/connectors/mcp/heartbeat");
|
|
498
|
-
assert.equal(calls[0]?.body.installToken, "atw_mcp_startup");
|
|
499
|
-
assert.equal(calls[0]?.body.clientId, "codex");
|
|
500
|
-
assert.equal(calls[1]?.url, "https://askthew.example.com/api/ingest/mcp");
|
|
501
|
-
assert.equal(calls[1]?.body.installToken, "atw_mcp_startup");
|
|
502
|
-
assert.equal(calls[1]?.body.clientId, "codex");
|
|
503
|
-
const signal = calls[1]?.body.sessionSignal;
|
|
504
|
-
assert.equal(signal.kind, "setup_complete");
|
|
505
|
-
assert.equal(signal.sequence, 0);
|
|
506
|
-
assert.match(signal.sessionId, /^mcp-startup:codex:/);
|
|
507
|
-
assert.equal(signal.metadata.automated, true);
|
|
508
|
-
assert.equal(signal.metadata.operational, true);
|
|
509
|
-
assert.equal(signal.metadata.origin, "mcp_server_startup");
|
|
510
|
-
assert.equal(signal.metadata.test_scope, "startup");
|
|
511
|
-
});
|
|
512
|
-
test("createAskTheWMcpServer accepts hosted connector identity overrides", async () => {
|
|
513
|
-
const calls = [];
|
|
514
|
-
const server = createAskTheWMcpServer({
|
|
515
|
-
apiBaseUrl: "https://askthew.example.com",
|
|
516
|
-
sendStartupHeartbeat: false,
|
|
517
|
-
credentials: {
|
|
518
|
-
installToken: "atw_mcp_remote",
|
|
519
|
-
clientId: "claude_remote",
|
|
520
|
-
clientLabel: "Claude Desktop / Cowork",
|
|
521
|
-
serverName: "askthew_workspace_a",
|
|
522
|
-
},
|
|
523
|
-
runtimeMetadata: {
|
|
524
|
-
connector_mode: "remote_mcp",
|
|
525
|
-
},
|
|
526
|
-
fetchImpl: (async (url, init) => {
|
|
527
|
-
calls.push({
|
|
528
|
-
url: String(url),
|
|
529
|
-
body: JSON.parse(String(init?.body ?? "{}")),
|
|
530
|
-
});
|
|
531
|
-
return new Response(JSON.stringify({ ok: true }), { status: 200 });
|
|
532
|
-
}),
|
|
533
|
-
});
|
|
534
|
-
const tool = server._registeredTools.capture_session_signal;
|
|
535
|
-
assert.ok(tool);
|
|
536
|
-
await tool.handler({
|
|
537
|
-
sessionId: "session-remote",
|
|
538
|
-
sequence: 1,
|
|
539
|
-
kind: "setup_complete",
|
|
540
|
-
summary: "Remote connector setup complete.",
|
|
541
|
-
evidence: [],
|
|
542
|
-
filesTouched: [],
|
|
543
|
-
commandsRun: [],
|
|
544
|
-
metadata: {},
|
|
545
|
-
});
|
|
546
|
-
assert.equal(calls[0]?.url, "https://askthew.example.com/api/ingest/mcp?response_shape=v2");
|
|
547
|
-
assert.equal(calls[0]?.body.installToken, "atw_mcp_remote");
|
|
548
|
-
assert.equal(calls[0]?.body.clientId, "claude_remote");
|
|
549
|
-
assert.equal(calls[0]?.body.clientLabel, "Claude Desktop / Cowork");
|
|
550
|
-
assert.equal(calls[0]?.body.sessionSignal.metadata.connector_mode, "remote_mcp");
|
|
551
|
-
});
|
|
552
|
-
test("free capture defaults to compact response and echo full returns full local payload", async () => {
|
|
553
|
-
await withFreeEnv(async () => {
|
|
554
|
-
const server = createAskTheWMcpServer({ sendStartupHeartbeat: false });
|
|
555
|
-
const capture = server._registeredTools.capture_session_signal;
|
|
556
|
-
const basePayload = {
|
|
557
|
-
sessionId: "session-compact",
|
|
558
|
-
sequence: 1,
|
|
559
|
-
kind: "implementation_update",
|
|
560
|
-
summary: "Updated compact response shape.",
|
|
561
|
-
evidence: [],
|
|
562
|
-
filesTouched: ["src/index.ts"],
|
|
563
|
-
commandsRun: ["npm test"],
|
|
564
|
-
metadata: {},
|
|
565
|
-
};
|
|
566
|
-
const compact = toolResultJson(await capture.handler(basePayload));
|
|
567
|
-
assert.equal(compact.ok, true);
|
|
568
|
-
assert.equal(compact.sessionId, "session-compact");
|
|
569
|
-
assert.equal(compact.sequence, 1);
|
|
570
|
-
assert.equal(typeof compact.id, "number");
|
|
571
|
-
assert.ok(JSON.stringify(compact).length < 1024);
|
|
572
|
-
assert.equal("signal" in compact, false);
|
|
573
|
-
const full = toolResultJson(await capture.handler({ ...basePayload, sequence: 2, echo: "full" }));
|
|
574
|
-
assert.equal(full.ok, true);
|
|
575
|
-
assert.equal(full.signal.summary, "Updated compact response shape.");
|
|
576
|
-
assert.equal(full.signal.sessionId, "session-compact");
|
|
577
|
-
});
|
|
578
|
-
});
|
|
579
|
-
test("stale free install without identity self-heals into local capture without hosted workspace calls", async () => {
|
|
580
|
-
await withPendingFreeEnv(async () => {
|
|
581
|
-
const calls = [];
|
|
582
|
-
const server = createAskTheWMcpServer({
|
|
583
|
-
apiBaseUrl: "https://askthew.example.com",
|
|
584
|
-
fetchImpl: async (url) => {
|
|
585
|
-
calls.push({ url: String(url) });
|
|
586
|
-
return new Response(JSON.stringify({ ok: true }), { status: 200 });
|
|
587
|
-
},
|
|
588
|
-
});
|
|
589
|
-
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
590
|
-
const tools = server._registeredTools;
|
|
591
|
-
const capture = toolResultJson(await tools.capture_session_signal.handler({
|
|
592
|
-
sessionId: "session-pending",
|
|
593
|
-
sequence: 1,
|
|
594
|
-
kind: "setup_complete",
|
|
595
|
-
summary: "Stale free install should capture locally.",
|
|
596
|
-
evidence: [],
|
|
597
|
-
filesTouched: [],
|
|
598
|
-
commandsRun: [],
|
|
599
|
-
metadata: {},
|
|
600
|
-
}));
|
|
601
|
-
assert.equal(capture.ok, true);
|
|
602
|
-
assert.equal(capture.id, 1);
|
|
603
|
-
assert.equal(calls.length, 1);
|
|
604
|
-
assert.match(calls[0].url, /\/api\/cli\/v1\/free-installs\/register$/);
|
|
605
|
-
const store = LocalStore.open();
|
|
606
|
-
try {
|
|
607
|
-
const stats = store.stats();
|
|
608
|
-
assert.equal(stats.signals, 1);
|
|
609
|
-
assert.equal(stats.decisions, 0);
|
|
610
|
-
}
|
|
611
|
-
finally {
|
|
612
|
-
store.close();
|
|
613
|
-
}
|
|
614
|
-
});
|
|
615
|
-
});
|
|
616
|
-
test("authenticated free mode keeps capture, decisions, and review local without hosted calls", async () => {
|
|
617
|
-
await withFreeEnv(async () => {
|
|
618
|
-
const calls = [];
|
|
619
|
-
const server = createAskTheWMcpServer({
|
|
620
|
-
apiBaseUrl: "https://askthew.example.com",
|
|
621
|
-
fetchImpl: async (url) => {
|
|
622
|
-
calls.push({ url: String(url) });
|
|
623
|
-
return new Response(JSON.stringify({ ok: true }), { status: 200 });
|
|
624
|
-
},
|
|
625
|
-
});
|
|
626
|
-
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
627
|
-
const tools = server._registeredTools;
|
|
628
|
-
const capture = toolResultJson(await tools.capture_session_signal.handler({
|
|
629
|
-
sessionId: "session-local-only",
|
|
630
|
-
sequence: 1,
|
|
631
|
-
kind: "implementation_update",
|
|
632
|
-
summary: "Captured locally after free auth.",
|
|
633
|
-
evidence: [],
|
|
634
|
-
filesTouched: ["packages/mcp-plugin/src/index.ts"],
|
|
635
|
-
commandsRun: [],
|
|
636
|
-
metadata: {},
|
|
637
|
-
}));
|
|
638
|
-
const decision = toolResultJson(await tools.create_decision.handler({ content: "Keep free mode local-only." }));
|
|
639
|
-
const review = toolResultJson(await tools.review_session.handler({ sessionId: "session-local-only", format: "json" }));
|
|
640
|
-
assert.equal(capture.ok, true);
|
|
641
|
-
assert.match(decision.id, /^d_/);
|
|
642
|
-
assert.equal(review.ok, true);
|
|
643
|
-
assert.equal(calls.length, 1);
|
|
644
|
-
assert.match(calls[0].url, /\/api\/cli\/v1\/free-installs\/register$/);
|
|
645
|
-
const store = LocalStore.open();
|
|
646
|
-
try {
|
|
647
|
-
const stats = store.stats();
|
|
648
|
-
assert.equal(stats.signals, 1);
|
|
649
|
-
assert.equal(stats.decisions, 1);
|
|
650
|
-
}
|
|
651
|
-
finally {
|
|
652
|
-
store.close();
|
|
653
|
-
}
|
|
654
|
-
});
|
|
655
|
-
});
|
|
656
|
-
test("installed free mode with credential file captures locally even if hosted app would return local-only 403", async () => {
|
|
657
|
-
await withInstalledFreeEnv(async () => {
|
|
658
|
-
const calls = [];
|
|
659
|
-
const server = createAskTheWMcpServer({
|
|
660
|
-
apiBaseUrl: "https://askthew.example.com",
|
|
661
|
-
fetchImpl: async (url) => {
|
|
662
|
-
calls.push({ url: String(url) });
|
|
663
|
-
return new Response(JSON.stringify({
|
|
664
|
-
ok: false,
|
|
665
|
-
code: "local_only_free_feature",
|
|
666
|
-
message: "This free MCP token is local-only and cannot read or write the shared Ask The W workspace.",
|
|
667
|
-
}), { status: 403 });
|
|
668
|
-
},
|
|
669
|
-
});
|
|
670
|
-
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
671
|
-
const tools = server._registeredTools;
|
|
672
|
-
const capture = toolResultJson(await tools.capture_session_signal.handler({
|
|
673
|
-
sessionId: "session-installed-free",
|
|
674
|
-
sequence: 1,
|
|
675
|
-
kind: "setup_complete",
|
|
676
|
-
summary: "Installed free mode should write this locally.",
|
|
677
|
-
evidence: [],
|
|
678
|
-
filesTouched: [],
|
|
679
|
-
commandsRun: [],
|
|
680
|
-
metadata: {},
|
|
681
|
-
}));
|
|
682
|
-
assert.equal(capture.ok, true);
|
|
683
|
-
assert.equal(capture.sessionId, "session-installed-free");
|
|
684
|
-
assert.equal("code" in capture, false);
|
|
685
|
-
assert.equal(JSON.stringify(capture).includes("local_only_free_feature"), false);
|
|
686
|
-
assert.equal(calls.length, 1);
|
|
687
|
-
assert.match(calls[0].url, /\/api\/cli\/v1\/free-installs\/register$/);
|
|
688
|
-
const store = LocalStore.open();
|
|
689
|
-
try {
|
|
690
|
-
const signals = store.listSignals({ sessionId: "session-installed-free", limit: 10 });
|
|
691
|
-
assert.equal(signals.length, 1);
|
|
692
|
-
assert.equal(signals[0]?.summary, "Installed free mode should write this locally.");
|
|
693
|
-
}
|
|
694
|
-
finally {
|
|
695
|
-
store.close();
|
|
696
|
-
}
|
|
697
|
-
});
|
|
698
|
-
});
|
|
699
|
-
test("free decisions, recap, coach, and promote return human-readable compact outputs", async () => {
|
|
700
|
-
await withFreeEnv(async () => {
|
|
701
|
-
const server = createAskTheWMcpServer({ sendStartupHeartbeat: false });
|
|
702
|
-
const tools = server._registeredTools;
|
|
703
|
-
await tools.capture_session_signal.handler({
|
|
704
|
-
sessionId: "session-free",
|
|
705
|
-
sequence: 1,
|
|
706
|
-
kind: "implementation_update",
|
|
707
|
-
summary: "Implemented the trial onboarding CTA.",
|
|
708
|
-
evidence: [],
|
|
709
|
-
filesTouched: ["apps/app/page.tsx"],
|
|
710
|
-
commandsRun: [],
|
|
711
|
-
metadata: {},
|
|
712
|
-
});
|
|
713
|
-
await tools.capture_session_signal.handler({
|
|
714
|
-
sessionId: "session-free",
|
|
715
|
-
sequence: 2,
|
|
716
|
-
kind: "verification_result",
|
|
717
|
-
summary: "npm test passed.",
|
|
718
|
-
evidence: [],
|
|
719
|
-
filesTouched: ["apps/app/page.tsx"],
|
|
720
|
-
commandsRun: ["npm test"],
|
|
721
|
-
metadata: {},
|
|
722
|
-
});
|
|
723
|
-
const created = toolResultJson(await tools.create_decision.handler({ content: "Keep the direct onboarding CTA." }));
|
|
724
|
-
assert.equal(created.ok, true);
|
|
725
|
-
assert.match(created.id, /^d_/);
|
|
726
|
-
assert.equal(created.sequence, 1);
|
|
727
|
-
const promoted = toolResultJson(await tools.promote_signal_to_decision.handler({ signalId: 1, status: "committed", why: "The captured implementation signal explains the change." }));
|
|
728
|
-
assert.equal(promoted.ok, true);
|
|
729
|
-
assert.equal(promoted.linkedSignalId, 1);
|
|
730
|
-
assert.equal(promoted.decision.rawContent, "Implemented the trial onboarding CTA.");
|
|
731
|
-
assert.deepEqual(promoted.decision.sourceSignalIds, [1]);
|
|
732
|
-
const digest = toolResultJson(await tools.recap.handler({ format: "digest" }));
|
|
733
|
-
const standup = toolResultJson(await tools.recap.handler({ format: "standup" }));
|
|
734
|
-
const share = toolResultJson(await tools.recap.handler({ format: "share" }));
|
|
735
|
-
assert.notEqual(digest.rendered, standup.rendered);
|
|
736
|
-
assert.notEqual(standup.rendered, share.rendered);
|
|
737
|
-
const coach = toolResultJson(await tools.coach.handler({ scope: "session" }));
|
|
738
|
-
assert.equal(coach.ok, true);
|
|
739
|
-
assert.equal(typeof coach.qualityScore, "number");
|
|
740
|
-
assert.match(coach.biggestGap, /\w/);
|
|
741
|
-
for (const scope of ["week", "patterns"]) {
|
|
742
|
-
const response = toolResultJson(await tools.coach.handler({ scope }));
|
|
743
|
-
assert.equal(response.ok, false);
|
|
744
|
-
assert.equal(response.code, "free_tier_paid_feature");
|
|
745
|
-
assert.equal(response.tool, "coach");
|
|
746
|
-
assert.match(response.upgradeUrl, /askthew\.com\/plugin/);
|
|
747
|
-
}
|
|
748
|
-
});
|
|
749
|
-
});
|
|
750
|
-
test("free local tools surface decision candidates, compact views, search, traversal, conflicts, and max_chars", async () => {
|
|
751
|
-
await withFreeEnv(async () => {
|
|
752
|
-
const server = createAskTheWMcpServer({ sendStartupHeartbeat: false });
|
|
753
|
-
const tools = server._registeredTools;
|
|
754
|
-
const captured = toolResultJson(await tools.capture_session_signal.handler({
|
|
755
|
-
sessionId: "session-candidates",
|
|
756
|
-
sequence: 1,
|
|
757
|
-
kind: "direction_change",
|
|
758
|
-
summary: "Let's go with token-budgeted local search because recap output is too large.",
|
|
759
|
-
evidence: [{
|
|
760
|
-
role: "assistant",
|
|
761
|
-
kind: "diff",
|
|
762
|
-
excerpt: "Changed search behavior.",
|
|
763
|
-
diff: "- old recap only\n+ new local search",
|
|
764
|
-
}],
|
|
765
|
-
filesTouched: ["packages/mcp-plugin/src/index.ts"],
|
|
766
|
-
commandsRun: [],
|
|
767
|
-
metadata: {},
|
|
768
|
-
}));
|
|
769
|
-
const candidates = toolResultJson(await tools.list_decision_candidates.handler({ sessionId: "session-candidates" }));
|
|
770
|
-
assert.equal(candidates.ok, true);
|
|
771
|
-
assert.equal(candidates.decisionCandidates.length, 1);
|
|
772
|
-
assert.equal(candidates.decisionCandidates[0].signalId, captured.id);
|
|
773
|
-
const promoted = toolResultJson(await tools.promote_signal_to_decision.handler({
|
|
774
|
-
signalId: captured.id,
|
|
775
|
-
status: "committed",
|
|
776
|
-
why: "The direction change has the rationale.",
|
|
777
|
-
}));
|
|
778
|
-
assert.equal(promoted.ok, true);
|
|
779
|
-
assert.equal(promoted.decision.contributingSignals[0].id, captured.id);
|
|
780
|
-
assert.equal(Boolean(promoted.decision.committedAt), true);
|
|
781
|
-
const signal = toolResultJson(await tools.get_signal.handler({ id: String(captured.id) }));
|
|
782
|
-
assert.equal(signal.signal.decision.id, promoted.id);
|
|
783
|
-
const decision = toolResultJson(await tools.get_decision.handler({ id: promoted.id }));
|
|
784
|
-
assert.equal(decision.decision.contributingSignals[0].id, captured.id);
|
|
785
|
-
const compact = toolResultJson(await tools.list_signals.handler({ sessionId: "session-candidates", compact: true }));
|
|
786
|
-
assert.deepEqual(Object.keys(compact.signals[0]).sort(), ["decisionId", "files", "id", "kind", "summary"]);
|
|
787
|
-
const found = toolResultJson(await tools.find_signal_by_summary.handler({ query: "token-budgeted" }));
|
|
788
|
-
assert.equal(found.ok, true);
|
|
789
|
-
assert.equal(found.signals[0].id, captured.id);
|
|
790
|
-
const search = toolResultJson(await tools.search_trail.handler({ query: "local search", compact: true }));
|
|
791
|
-
assert.equal(search.ok, true);
|
|
792
|
-
assert.equal(search.matches.length >= 1, true);
|
|
793
|
-
const conflict = toolResultJson(await tools.create_decision.handler({
|
|
794
|
-
content: "Drop token-budgeted local search.",
|
|
795
|
-
}));
|
|
796
|
-
assert.equal(conflict.ok, true);
|
|
797
|
-
assert.equal(conflict.warnings[0].code, "possible_conflict");
|
|
798
|
-
assert.equal(conflict.warnings[0].conflictingDecisionId, promoted.id);
|
|
799
|
-
const budgeted = toolResultJson(await tools.review_session.handler({
|
|
800
|
-
sessionId: "session-candidates",
|
|
801
|
-
format: "json",
|
|
802
|
-
max_chars: 500,
|
|
803
|
-
}));
|
|
804
|
-
assert.equal(budgeted.ok, true);
|
|
805
|
-
assert.equal(budgeted.truncated, true);
|
|
806
|
-
assert.equal(JSON.stringify(budgeted, null, 2).length <= 700, true);
|
|
807
|
-
});
|
|
808
|
-
});
|
|
809
|
-
test("free-tier smoke covers every free tool without overflow or silent failures", async () => {
|
|
810
|
-
await withFreeEnv(async () => {
|
|
811
|
-
const server = createAskTheWMcpServer({ sendStartupHeartbeat: false });
|
|
812
|
-
const tools = server._registeredTools;
|
|
813
|
-
const results = {};
|
|
814
|
-
const capture = toolResultJson(await tools.capture_session_signal.handler({
|
|
815
|
-
sessionId: "session-smoke",
|
|
816
|
-
sequence: 1,
|
|
817
|
-
kind: "implementation_update",
|
|
818
|
-
summary: "Implemented every free tool smoke path.",
|
|
819
|
-
evidence: [],
|
|
820
|
-
filesTouched: ["packages/mcp-plugin/src/index.ts"],
|
|
821
|
-
commandsRun: ["npm test --workspace @askthew/mcp-plugin"],
|
|
822
|
-
metadata: {},
|
|
823
|
-
}));
|
|
824
|
-
results.capture_session_signal = capture;
|
|
825
|
-
assert.equal(capture.ok, true);
|
|
826
|
-
results.list_signals = toolResultJson(await tools.list_signals.handler({ limit: 10 }));
|
|
827
|
-
assert.equal(results.list_signals.ok, true);
|
|
828
|
-
assert.equal(results.list_signals.signals.length, 1);
|
|
829
|
-
results.get_signal = toolResultJson(await tools.get_signal.handler({ id: String(capture.id) }));
|
|
830
|
-
assert.equal(results.get_signal.ok, true);
|
|
831
|
-
assert.equal(results.get_signal.signal.summary, "Implemented every free tool smoke path.");
|
|
832
|
-
const created = toolResultJson(await tools.create_decision.handler({ content: "Keep free-tool smoke coverage explicit.", echo: "summary" }));
|
|
833
|
-
results.create_decision = created;
|
|
834
|
-
assert.equal(created.ok, true);
|
|
835
|
-
assert.match(created.id, /^d_/);
|
|
836
|
-
results.get_decision = toolResultJson(await tools.get_decision.handler({ id: created.id }));
|
|
837
|
-
assert.equal(results.get_decision.ok, true);
|
|
838
|
-
results.update_decision = toolResultJson(await tools.update_decision.handler({
|
|
839
|
-
id: created.id,
|
|
840
|
-
headline: "Keep free-tool smoke coverage explicit",
|
|
841
|
-
why: "Acceptance requires every free tool to run.",
|
|
842
|
-
status: "committed",
|
|
843
|
-
echo: "summary",
|
|
844
|
-
}));
|
|
845
|
-
assert.equal(results.update_decision.ok, true);
|
|
846
|
-
results.list_decisions = toolResultJson(await tools.list_decisions.handler({ limit: 10 }));
|
|
847
|
-
assert.equal(results.list_decisions.ok, true);
|
|
848
|
-
assert.equal(results.list_decisions.decisions.length >= 1, true);
|
|
849
|
-
results.review_decisions = toolResultJson(await tools.review_decisions.handler({ format: "markdown", limit: 10 }));
|
|
850
|
-
assert.equal(results.review_decisions.ok, true);
|
|
851
|
-
assert.match(results.review_decisions.rendered, /# Decisions/);
|
|
852
|
-
results.review_session = toolResultJson(await tools.review_session.handler({ sessionId: "session-smoke", format: "markdown" }));
|
|
853
|
-
assert.equal(results.review_session.ok, true);
|
|
854
|
-
assert.match(results.review_session.rendered, /Session Review/);
|
|
855
|
-
results.view_timeline = toolResultJson(await tools.view_timeline.handler({ scope: "session" }));
|
|
856
|
-
assert.equal(results.view_timeline.ok, true);
|
|
857
|
-
assert.equal(results.view_timeline.points.some((point) => point.x === "Other"), false);
|
|
858
|
-
results.recap = toolResultJson(await tools.recap.handler({ sessionId: "session-smoke", format: "digest" }));
|
|
859
|
-
assert.equal(results.recap.ok, true);
|
|
860
|
-
assert.match(results.recap.rendered, /Session Digest/);
|
|
861
|
-
results.coach = toolResultJson(await tools.coach.handler({ sessionId: "session-smoke", scope: "session" }));
|
|
862
|
-
assert.equal(results.coach.ok, true);
|
|
863
|
-
assert.match(results.coach.rendered, /Decision quality score/);
|
|
864
|
-
results.promote_signal_to_decision = toolResultJson(await tools.promote_signal_to_decision.handler({
|
|
865
|
-
signalId: capture.id,
|
|
866
|
-
status: "committed",
|
|
867
|
-
why: "The signal records the implementation.",
|
|
868
|
-
}));
|
|
869
|
-
assert.equal(results.promote_signal_to_decision.ok, true);
|
|
870
|
-
assert.equal(results.promote_signal_to_decision.linkedSignalId, capture.id);
|
|
871
|
-
results.delete_decision = toolResultJson(await tools.delete_decision.handler({
|
|
872
|
-
id: created.id,
|
|
873
|
-
confirmText: created.id,
|
|
874
|
-
}));
|
|
875
|
-
assert.equal(results.delete_decision.ok, true);
|
|
876
|
-
for (const [toolName, result] of Object.entries(results)) {
|
|
877
|
-
assert.equal(result.ok, true, toolName);
|
|
878
|
-
assert.equal(JSON.stringify(result).length < 15000, true, `${toolName} should stay compact`);
|
|
879
|
-
}
|
|
880
|
-
});
|
|
881
|
-
});
|
|
882
|
-
test("free review_session markdown is capped and json is cursor-paginated with a 3-session limit", async () => {
|
|
883
|
-
await withFreeEnv(async () => {
|
|
884
|
-
const server = createAskTheWMcpServer({ sendStartupHeartbeat: false });
|
|
885
|
-
const tools = server._registeredTools;
|
|
886
|
-
for (let sessionIndex = 1; sessionIndex <= 4; sessionIndex += 1) {
|
|
887
|
-
for (let sequence = 1; sequence <= 55; sequence += 1) {
|
|
888
|
-
await tools.capture_session_signal.handler({
|
|
889
|
-
sessionId: `session-${sessionIndex}`,
|
|
890
|
-
sequence,
|
|
891
|
-
kind: sequence % 5 === 0 ? "verification_result" : "session_checkpoint",
|
|
892
|
-
summary: `Signal ${sequence} for session ${sessionIndex}`,
|
|
893
|
-
evidence: [],
|
|
894
|
-
filesTouched: [],
|
|
895
|
-
commandsRun: [],
|
|
896
|
-
metadata: {},
|
|
897
|
-
});
|
|
898
|
-
}
|
|
899
|
-
}
|
|
900
|
-
const markdown = toolResultJson(await tools.review_session.handler({ sessionId: "session-4", format: "markdown" }));
|
|
901
|
-
assert.equal(markdown.ok, true);
|
|
902
|
-
assert.equal(markdown.rendered.split("\n").length <= 300, true);
|
|
903
|
-
assert.match(markdown.rendered, /Signals By Kind/);
|
|
904
|
-
assert.equal("signals" in markdown, false);
|
|
905
|
-
const json = toolResultJson(await tools.review_session.handler({ sessionId: "session-4", format: "json" }));
|
|
906
|
-
assert.equal(json.ok, true);
|
|
907
|
-
assert.equal(json.signals.length, 50);
|
|
908
|
-
assert.equal(typeof json.nextCursor, "string");
|
|
909
|
-
const capped = toolResultJson(await tools.review_session.handler({ sessionId: "session-1", format: "markdown" }));
|
|
910
|
-
assert.equal(capped.ok, false);
|
|
911
|
-
assert.equal(capped.code, "free_tier_limit");
|
|
912
|
-
assert.equal(capped.limit, 3);
|
|
913
|
-
assert.match(capped.upgradeUrl, /askthew\.com\/plugin/);
|
|
914
|
-
});
|
|
915
|
-
});
|
|
916
|
-
test("paid tools return canonical paywall envelope before transport in free mode", async () => {
|
|
917
|
-
await withFreeEnv(async () => {
|
|
918
|
-
const server = createAskTheWMcpServer({ sendStartupHeartbeat: false });
|
|
919
|
-
const tools = server._registeredTools;
|
|
920
|
-
for (const toolName of [
|
|
921
|
-
"list_outcomes",
|
|
922
|
-
"get_outcome",
|
|
923
|
-
"list_outcome_signals",
|
|
924
|
-
"create_outcome",
|
|
925
|
-
"update_outcome",
|
|
926
|
-
"delete_outcome",
|
|
927
|
-
"get_north_star",
|
|
928
|
-
"update_north_star",
|
|
929
|
-
"export_decisions",
|
|
930
|
-
]) {
|
|
931
|
-
const payload = toolName === "get_outcome" || toolName === "list_outcome_signals"
|
|
932
|
-
? { id: "o1" }
|
|
933
|
-
: toolName === "create_outcome"
|
|
934
|
-
? { name: "Reduce churn" }
|
|
935
|
-
: toolName === "update_outcome"
|
|
936
|
-
? { id: "o1", summary: "New" }
|
|
937
|
-
: toolName === "delete_outcome"
|
|
938
|
-
? { id: "o1", confirmText: "Reduce churn" }
|
|
939
|
-
: toolName === "update_north_star"
|
|
940
|
-
? { metric: "Active users", current: "1", target: "10", reason: "test" }
|
|
941
|
-
: {};
|
|
942
|
-
const response = toolResultJson(await tools[toolName].handler(payload));
|
|
943
|
-
assert.equal(response.ok, false, toolName);
|
|
944
|
-
assert.equal(response.code, "free_tier_paid_feature", toolName);
|
|
945
|
-
assert.equal(response.tool, toolName, toolName);
|
|
946
|
-
assert.match(response.upgradeUrl, /askthew\.com\/plugin/, toolName);
|
|
947
|
-
assert.equal(response.supportEmail, "support@askthew.com", toolName);
|
|
948
|
-
}
|
|
949
|
-
});
|
|
950
|
-
});
|