@intentic/extension-api 1.325.0 → 1.327.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +26 -1
- package/dist/api.d.ts +2 -3
- package/dist/api.d.ts.map +1 -1
- package/dist/background.d.ts +4 -9
- package/dist/background.d.ts.map +1 -1
- package/dist/background.js.map +1 -1
- package/dist/diff.d.ts +2 -2
- package/dist/diff.d.ts.map +1 -1
- package/dist/disposable.d.ts +4 -0
- package/dist/disposable.d.ts.map +1 -0
- package/dist/disposable.js +2 -0
- package/dist/disposable.js.map +1 -0
- package/dist/runtime.d.ts +49 -0
- package/dist/runtime.d.ts.map +1 -0
- package/dist/runtime.js +359 -0
- package/dist/runtime.js.map +1 -0
- package/dist/server.d.ts +40 -2
- package/dist/server.d.ts.map +1 -1
- package/dist/stream.d.ts.map +1 -1
- package/dist/stream.js +2 -50
- package/dist/stream.js.map +1 -1
- package/dist/testing.d.ts +47 -0
- package/dist/testing.d.ts.map +1 -0
- package/dist/testing.js +152 -0
- package/dist/testing.js.map +1 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.d.ts.map +1 -1
- package/dist/version.js +1 -1
- package/dist/version.js.map +1 -1
- package/package.json +41 -5
- package/src/api.ts +2 -3
- package/src/background.ts +5 -12
- package/src/diff.ts +4 -4
- package/src/disposable.ts +5 -0
- package/src/runtime.integration.test.ts +159 -0
- package/src/runtime.ts +535 -0
- package/src/server.ts +91 -4
- package/src/stream.ts +4 -55
- package/src/surface.json +482 -0
- package/src/testing.ts +237 -0
- package/src/version.ts +23 -1
package/dist/testing.js
ADDED
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import { mkdtemp, rm } from "node:fs/promises";
|
|
2
|
+
import { tmpdir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { extensionRouteReach } from "@intentic/extension-manifest/permissions";
|
|
5
|
+
import { extensionEventsUrl, extensionSettingsUrl } from "@intentic/sandbox-contract/extension-protocol";
|
|
6
|
+
import { EXTENSION_TOKEN_HEADER } from "@intentic/sandbox-contract/headers";
|
|
7
|
+
import { activateServerModule, createServerApi, deactivateWithin, healthWithin } from "./runtime.js";
|
|
8
|
+
const DAEMON = "http://daemon.fake";
|
|
9
|
+
const TOKEN = "fake-extension-token";
|
|
10
|
+
const json = (body, status = 200) => Response.json(body, { status });
|
|
11
|
+
const changedKeys = (before, after) => [...new Set([...Object.keys(before), ...Object.keys(after)])].filter((key) => before[key] !== after[key]).toSorted();
|
|
12
|
+
export const fakeExtensionApi = async (options) => {
|
|
13
|
+
const made = [];
|
|
14
|
+
const dirOf = async (given, label) => {
|
|
15
|
+
if (given !== undefined) {
|
|
16
|
+
return given;
|
|
17
|
+
}
|
|
18
|
+
const dir = await mkdtemp(join(tmpdir(), `fake-extension-${label}-`));
|
|
19
|
+
made.push(dir);
|
|
20
|
+
return dir;
|
|
21
|
+
};
|
|
22
|
+
const workspaceRoot = await dirOf(options.workspaceRoot, "workspace");
|
|
23
|
+
const id = `${options.manifest.publisher}.${options.manifest.name}`;
|
|
24
|
+
const reach = { permissions: options.manifest.permissions?.daemon ?? [], listener: options.manifest.contributes?.listener?.provider };
|
|
25
|
+
const calls = [];
|
|
26
|
+
const logs = [];
|
|
27
|
+
let settings = { ...options.settings };
|
|
28
|
+
const streams = new Set();
|
|
29
|
+
const pending = [];
|
|
30
|
+
const encoder = new TextEncoder();
|
|
31
|
+
const write = (stream, event) => {
|
|
32
|
+
try {
|
|
33
|
+
stream.enqueue(encoder.encode(`${JSON.stringify(event)}\n`));
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
streams.delete(stream);
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
const send = (event) => {
|
|
40
|
+
if (streams.size === 0) {
|
|
41
|
+
pending.push(event);
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
for (const stream of streams) {
|
|
45
|
+
write(stream, event);
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
const eventStream = (signal) => {
|
|
49
|
+
let held;
|
|
50
|
+
const body = new ReadableStream({
|
|
51
|
+
start: (controller) => {
|
|
52
|
+
held = controller;
|
|
53
|
+
streams.add(controller);
|
|
54
|
+
write(controller, { kind: "heartbeat" });
|
|
55
|
+
for (const event of pending.splice(0)) {
|
|
56
|
+
write(controller, event);
|
|
57
|
+
}
|
|
58
|
+
},
|
|
59
|
+
cancel: () => {
|
|
60
|
+
if (held !== undefined) {
|
|
61
|
+
streams.delete(held);
|
|
62
|
+
}
|
|
63
|
+
},
|
|
64
|
+
});
|
|
65
|
+
signal?.addEventListener("abort", () => {
|
|
66
|
+
if (held !== undefined) {
|
|
67
|
+
streams.delete(held);
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
return new Response(body, { headers: { "content-type": "application/x-ndjson" } });
|
|
71
|
+
};
|
|
72
|
+
const daemon = async (input, init) => {
|
|
73
|
+
const request = new Request(input, init);
|
|
74
|
+
const url = new URL(request.url);
|
|
75
|
+
const line = `${request.method} ${url.pathname}`;
|
|
76
|
+
if (request.headers.get(EXTENSION_TOKEN_HEADER) !== TOKEN) {
|
|
77
|
+
calls.push({ line, admitted: false });
|
|
78
|
+
return json({ error: "unauthorized" }, 401);
|
|
79
|
+
}
|
|
80
|
+
const admitted = extensionRouteReach(reach, request.method, url.pathname);
|
|
81
|
+
calls.push({ line, admitted });
|
|
82
|
+
if (!admitted) {
|
|
83
|
+
return json({ error: "extension token not valid for this route" }, 403);
|
|
84
|
+
}
|
|
85
|
+
if (request.method === "GET" && url.pathname === extensionSettingsUrl()) {
|
|
86
|
+
return json({ settings });
|
|
87
|
+
}
|
|
88
|
+
if (request.method === "GET" && url.pathname === extensionEventsUrl()) {
|
|
89
|
+
return eventStream(init?.signal ?? null);
|
|
90
|
+
}
|
|
91
|
+
return (await options.daemon?.(request)) ?? json({ error: "not found" }, 404);
|
|
92
|
+
};
|
|
93
|
+
const slots = createServerApi({
|
|
94
|
+
id,
|
|
95
|
+
daemonUrl: DAEMON,
|
|
96
|
+
token: TOKEN,
|
|
97
|
+
permissions: reach.permissions,
|
|
98
|
+
workspaceRoot,
|
|
99
|
+
extensionDir: await dirOf(options.extensionDir, "checkout"),
|
|
100
|
+
stateDir: await dirOf(options.stateDir, "state"),
|
|
101
|
+
cacheDir: await dirOf(options.cacheDir, "cache"),
|
|
102
|
+
log: (message) => logs.push(message),
|
|
103
|
+
fetch: daemon,
|
|
104
|
+
});
|
|
105
|
+
let activation = {};
|
|
106
|
+
return {
|
|
107
|
+
api: slots.api,
|
|
108
|
+
calls,
|
|
109
|
+
logs,
|
|
110
|
+
activate: async (module) => {
|
|
111
|
+
activation = await activateServerModule(module, slots.api, { extensionId: id });
|
|
112
|
+
},
|
|
113
|
+
route: async (path, init) => {
|
|
114
|
+
const handler = slots.handler();
|
|
115
|
+
if (handler === undefined) {
|
|
116
|
+
return json({ error: `the "${id}" backend serves no routes` }, 404);
|
|
117
|
+
}
|
|
118
|
+
return (await handler(new Request(new URL(path, "http://extension.internal"), init))) ?? json({ error: "not found" }, 404);
|
|
119
|
+
},
|
|
120
|
+
tools: async (card) => (await slots.tools()?.(card)) ?? [],
|
|
121
|
+
callTool: async (name, args = {}, card, context = {}) => {
|
|
122
|
+
const tool = ((await slots.tools()?.(card)) ?? []).find((each) => each.name === name);
|
|
123
|
+
if (tool === undefined) {
|
|
124
|
+
throw new Error(`"${id}" serves no tool named ${name}${card === undefined ? "" : ` for card ${card.id}`}`);
|
|
125
|
+
}
|
|
126
|
+
return tool.call(args, { signal: context.signal ?? new AbortController().signal, conversationId: context.conversationId });
|
|
127
|
+
},
|
|
128
|
+
setSettings: (values) => {
|
|
129
|
+
const keys = changedKeys(settings, values);
|
|
130
|
+
settings = { ...values };
|
|
131
|
+
if (keys.length > 0) {
|
|
132
|
+
send({ kind: "settings", keys });
|
|
133
|
+
}
|
|
134
|
+
},
|
|
135
|
+
emit: send,
|
|
136
|
+
health: () => healthWithin(activation),
|
|
137
|
+
deactivate: () => deactivateWithin(activation, (message) => logs.push(message)),
|
|
138
|
+
dispose: async () => {
|
|
139
|
+
slots.close();
|
|
140
|
+
for (const stream of streams) {
|
|
141
|
+
try {
|
|
142
|
+
stream.close();
|
|
143
|
+
}
|
|
144
|
+
catch {
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
streams.clear();
|
|
148
|
+
await Promise.all(made.map((dir) => rm(dir, { recursive: true, force: true })));
|
|
149
|
+
},
|
|
150
|
+
};
|
|
151
|
+
};
|
|
152
|
+
//# sourceMappingURL=testing.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"testing.js","sourceRoot":"","sources":["../src/testing.ts"],"names":[],"mappings":"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"}
|
package/dist/version.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export declare const extensionApiVersion = "2.
|
|
1
|
+
export declare const extensionApiVersion = "2.26.0";
|
|
2
2
|
//# sourceMappingURL=version.d.ts.map
|
package/dist/version.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"version.d.ts","sourceRoot":"","sources":["../src/version.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"version.d.ts","sourceRoot":"","sources":["../src/version.ts"],"names":[],"mappings":"AAwKA,eAAO,MAAM,mBAAmB,WAAW,CAAC"}
|
package/dist/version.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export const extensionApiVersion = "2.
|
|
1
|
+
export const extensionApiVersion = "2.26.0";
|
|
2
2
|
//# sourceMappingURL=version.js.map
|
package/dist/version.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"version.js","sourceRoot":"","sources":["../src/version.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"version.js","sourceRoot":"","sources":["../src/version.ts"],"names":[],"mappings":"AAwKA,MAAM,CAAC,MAAM,mBAAmB,GAAG,QAAQ,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@intentic/extension-api",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.327.0",
|
|
4
4
|
"description": "The versioned public API intentic extensions compile against, manifest schema, detection facts and the host API",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -39,24 +39,60 @@
|
|
|
39
39
|
"@intentic/src": "./src/protocol.ts",
|
|
40
40
|
"default": "./dist/protocol.js"
|
|
41
41
|
}
|
|
42
|
+
},
|
|
43
|
+
"./runtime": {
|
|
44
|
+
"types": "./dist/runtime.d.ts",
|
|
45
|
+
"import": {
|
|
46
|
+
"@intentic/src": "./src/runtime.ts",
|
|
47
|
+
"default": "./dist/runtime.js"
|
|
48
|
+
},
|
|
49
|
+
"require": {
|
|
50
|
+
"@intentic/src": "./src/runtime.ts",
|
|
51
|
+
"default": "./dist/runtime.js"
|
|
52
|
+
}
|
|
53
|
+
},
|
|
54
|
+
"./testing": {
|
|
55
|
+
"types": "./dist/testing.d.ts",
|
|
56
|
+
"import": {
|
|
57
|
+
"@intentic/src": "./src/testing.ts",
|
|
58
|
+
"default": "./dist/testing.js"
|
|
59
|
+
},
|
|
60
|
+
"require": {
|
|
61
|
+
"@intentic/src": "./src/testing.ts",
|
|
62
|
+
"default": "./dist/testing.js"
|
|
63
|
+
}
|
|
42
64
|
}
|
|
43
65
|
},
|
|
44
66
|
"dependencies": {
|
|
45
|
-
"@intentic/
|
|
67
|
+
"@intentic/base": "1.327.0",
|
|
68
|
+
"@intentic/extension-manifest": "1.327.0",
|
|
69
|
+
"@intentic/sandbox-contract": "1.327.0",
|
|
70
|
+
"@orpc/client": "1.15.4",
|
|
46
71
|
"@orpc/contract": "1.15.4",
|
|
72
|
+
"@orpc/openapi-client": "1.15.4",
|
|
47
73
|
"tslib": "2.8.1"
|
|
48
74
|
},
|
|
49
75
|
"peerDependencies": {
|
|
50
76
|
"vue": "3"
|
|
51
77
|
},
|
|
78
|
+
"peerDependenciesMeta": {
|
|
79
|
+
"vue": {
|
|
80
|
+
"optional": true
|
|
81
|
+
}
|
|
82
|
+
},
|
|
52
83
|
"devDependencies": {
|
|
84
|
+
"@intentic/testing": "0.0.0",
|
|
53
85
|
"@intentic/tsconfig": "0.0.0",
|
|
54
|
-
"@types/
|
|
86
|
+
"@types/bun": "1.4.2",
|
|
87
|
+
"@types/node": "24.19.1",
|
|
55
88
|
"@typescript/native-preview": "7.0.0-dev.20260707.2",
|
|
56
|
-
"vue": "3.5.43"
|
|
89
|
+
"vue": "3.5.43",
|
|
90
|
+
"zod": "4.6.5"
|
|
57
91
|
},
|
|
58
92
|
"scripts": {
|
|
59
93
|
"build": "tsgo",
|
|
60
|
-
"watch": "tsgo --build --watch"
|
|
94
|
+
"watch": "tsgo --build --watch",
|
|
95
|
+
"typecheck": "tsgo --noEmit -p tsconfig.test.json",
|
|
96
|
+
"test": "suites"
|
|
61
97
|
}
|
|
62
98
|
}
|
package/src/api.ts
CHANGED
|
@@ -2,14 +2,13 @@ import type { sandboxContract } from "@intentic/sandbox-contract";
|
|
|
2
2
|
import type { ContractRouterClient } from "@orpc/contract";
|
|
3
3
|
import type { Component } from "vue";
|
|
4
4
|
import type { DiffPayload } from "./diff.js";
|
|
5
|
+
import type { Disposable } from "./disposable.js";
|
|
5
6
|
import type { CapabilityFacts, RepoFacts } from "./facts.js";
|
|
6
7
|
|
|
7
8
|
// The host API an extension programs against; there is no ambient global. Arrives as
|
|
8
9
|
// activate(api, context); everything registered returns a Disposable pushed onto context.subscriptions.
|
|
9
10
|
|
|
10
|
-
export
|
|
11
|
-
dispose(): void;
|
|
12
|
-
}
|
|
11
|
+
export type { Disposable } from "./disposable.js";
|
|
13
12
|
|
|
14
13
|
// One sidebar element a view contributes, routed at /ext/<viewId>/<key> and rendered by the view's
|
|
15
14
|
// component with `repo` (+ props) bound.
|
package/src/background.ts
CHANGED
|
@@ -1,10 +1,8 @@
|
|
|
1
1
|
import {
|
|
2
2
|
at,
|
|
3
|
-
type Conversion,
|
|
4
3
|
drop,
|
|
5
4
|
dropAll,
|
|
6
5
|
fold,
|
|
7
|
-
type Granularity,
|
|
8
6
|
mapValue,
|
|
9
7
|
nested,
|
|
10
8
|
pinDefault,
|
|
@@ -16,6 +14,7 @@ import {
|
|
|
16
14
|
} from "@intentic/sandbox-contract/documents";
|
|
17
15
|
import type { Ref } from "vue";
|
|
18
16
|
import type { Disposable, IntenticApi } from "./api.js";
|
|
17
|
+
import type { ServerDocumentOptions, StoredJson } from "./server.js";
|
|
19
18
|
import { sandboxRef, sandboxScopeGuard } from "./scope.js";
|
|
20
19
|
|
|
21
20
|
// Sandbox-scoped background polling for a badge that needs an answer before its tile opens: state lives at module
|
|
@@ -249,15 +248,8 @@ export interface SandboxDocument<T> {
|
|
|
249
248
|
update(change: (current: T) => T): Promise<boolean>;
|
|
250
249
|
}
|
|
251
250
|
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
readonly parse: (raw: unknown) => T | undefined;
|
|
255
|
-
readonly fallback: () => T;
|
|
256
|
-
// Append-only: a conversion is never edited or removed once shipped, only followed by another.
|
|
257
|
-
readonly history?: readonly Conversion[];
|
|
258
|
-
// Where the conversions apply: the document, each entry of a top-level array, or each value of an object keyed by id.
|
|
259
|
-
readonly granularity?: Granularity;
|
|
260
|
-
}
|
|
251
|
+
// The same options a backend's `api.document` takes (server.ts): one file's shape, however the extension reaches it.
|
|
252
|
+
export type SandboxDocumentOptions<T> = ServerDocumentOptions<T>;
|
|
261
253
|
|
|
262
254
|
// Every handle on one path queues behind the others, not just behind itself: two handles opened on the same file (two
|
|
263
255
|
// panels, a panel and a command) would otherwise each read, change and write, and the second write would drop the first.
|
|
@@ -270,7 +262,8 @@ export const sandboxDocument = <T>(host: () => IntenticApi, path: string, option
|
|
|
270
262
|
const readAs = async () => {
|
|
271
263
|
// Through the contract's own read, which throws for a refused or unreachable read where `readJson` answers absent.
|
|
272
264
|
const answer = await host().sandbox.rpc.workspace.file({ path });
|
|
273
|
-
|
|
265
|
+
// SAFETY: readDocument hands its parse what JSON.parse produced and the conversions rewrote, which is JSON.
|
|
266
|
+
return answer.present ? readDocument<T>(answer.content, shape, { kind: `whole`, parse: (raw) => options.parse(raw as StoredJson) }) : undefined;
|
|
274
267
|
};
|
|
275
268
|
const read = async (): Promise<T> => {
|
|
276
269
|
try {
|
package/src/diff.ts
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
import type { PartialFileDiff } from "@intentic/sandbox-contract";
|
|
1
|
+
import type { ChangeStatus, PartialFileDiff } from "@intentic/sandbox-contract";
|
|
2
2
|
|
|
3
3
|
// The argument to `api.workspace.openDiff`: the extension says what changed, the host owns the tab, viewer and
|
|
4
4
|
// dirty-buffer bookkeeping. Lives in the public api package because the app's own review surfaces build the identical
|
|
5
5
|
// payload.
|
|
6
6
|
|
|
7
|
-
// Git's vocabulary for what happened to a file; the host renders each as its own
|
|
8
|
-
// git's unmerged state (`U`), not a kind of modification.
|
|
9
|
-
export type ChangeStatus
|
|
7
|
+
// Git's vocabulary for what happened to a file (the contract's ChangeStatusSchema); the host renders each as its own
|
|
8
|
+
// letter and colour. "conflicted" is git's unmerged state (`U`), not a kind of modification.
|
|
9
|
+
export type { ChangeStatus };
|
|
10
10
|
|
|
11
11
|
// A binary diff ships its two sides as daemon URLs to fetch bytes from, not as content. An absent side means that side
|
|
12
12
|
// does not exist; the viewer gives the pane entirely to the side that does.
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
|
|
2
|
+
import { tmpdir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { rename } from "@intentic/sandbox-contract/documents";
|
|
5
|
+
import { waitFor } from "@intentic/testing/bun";
|
|
6
|
+
import { z } from "zod";
|
|
7
|
+
import { connectExtensionProcess } from "./runtime.js";
|
|
8
|
+
import type { ExtensionServerApi, ServerActivation, StoredJson } from "./server.js";
|
|
9
|
+
import { type FakeExtension, fakeExtensionApi } from "./testing.js";
|
|
10
|
+
|
|
11
|
+
// The backend api as a server bundle and a process both get it (runtime.ts), driven through the SDK's own fake daemon
|
|
12
|
+
// (testing.ts), which judges every call by the reach rule the daemon's grant applies.
|
|
13
|
+
|
|
14
|
+
const opened: FakeExtension[] = [];
|
|
15
|
+
const fake = async (options: Parameters<typeof fakeExtensionApi>[0]): Promise<FakeExtension> => {
|
|
16
|
+
const made = await fakeExtensionApi(options);
|
|
17
|
+
opened.push(made);
|
|
18
|
+
return made;
|
|
19
|
+
};
|
|
20
|
+
afterEach(async () => {
|
|
21
|
+
await Promise.all(opened.splice(0).map((each) => each.dispose()));
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
const manifest = { publisher: "acme", name: "probe", permissions: { daemon: ["GET /ports"] } };
|
|
25
|
+
|
|
26
|
+
test("a call the manifest declares reaches the daemon, and one it does not is refused there", async () => {
|
|
27
|
+
const extension = await fake({ manifest, daemon: () => Response.json({ ports: [] }) });
|
|
28
|
+
expect((await extension.api.daemon.request("/ports")).status).toBe(200);
|
|
29
|
+
expect((await extension.api.daemon.request("/agents")).status).toBe(403);
|
|
30
|
+
expect(extension.calls).toEqual([
|
|
31
|
+
{ line: "GET /ports", admitted: true },
|
|
32
|
+
{ line: "GET /agents", admitted: false },
|
|
33
|
+
]);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
test("a typed call outside permissions.daemon is refused before anything is sent", async () => {
|
|
37
|
+
const extension = await fake({ manifest });
|
|
38
|
+
await expect(extension.api.daemon.rpc.extensions.settings({ id: "acme.probe" })).rejects.toThrow(/declare it in permissions\.daemon/);
|
|
39
|
+
expect(extension.calls).toEqual([]);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
test("its own settings and event stream need no declaration, and a change names the keys that moved", async () => {
|
|
43
|
+
const extension = await fake({ manifest: { publisher: "acme", name: "quiet" }, settings: { engine: "browser", autoStart: false } });
|
|
44
|
+
expect(await extension.api.settings.get()).toEqual({ engine: "browser", autoStart: false });
|
|
45
|
+
const heard: (readonly string[])[] = [];
|
|
46
|
+
const subscription = extension.api.settings.onDidChange((keys) => heard.push(keys));
|
|
47
|
+
extension.setSettings({ engine: "server", autoStart: false });
|
|
48
|
+
await waitFor(() => expect(heard).toHaveLength(1));
|
|
49
|
+
expect(heard).toEqual([["engine"]]);
|
|
50
|
+
expect(await extension.api.settings.get()).toEqual({ engine: "server", autoStart: false });
|
|
51
|
+
subscription.dispose();
|
|
52
|
+
expect(extension.calls.every((call) => call.admitted)).toBe(true);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
test("workspace changes arrive as the daemon frames them, and a listener that throws does not stop the rest", async () => {
|
|
56
|
+
const extension = await fake({ manifest });
|
|
57
|
+
const files: (readonly string[])[] = [];
|
|
58
|
+
const refs: (readonly string[])[] = [];
|
|
59
|
+
extension.api.workspace.onDidChangeFiles(() => {
|
|
60
|
+
throw new Error("boom");
|
|
61
|
+
});
|
|
62
|
+
extension.api.workspace.onDidChangeFiles((paths) => files.push(paths));
|
|
63
|
+
extension.api.workspace.onDidChangeRefs((repos) => refs.push(repos));
|
|
64
|
+
extension.emit({ kind: "files", paths: ["src/a.ts"] });
|
|
65
|
+
extension.emit({ kind: "refs", repos: ["app"] });
|
|
66
|
+
await waitFor(() => {
|
|
67
|
+
expect(files).toHaveLength(1);
|
|
68
|
+
expect(refs).toHaveLength(1);
|
|
69
|
+
});
|
|
70
|
+
expect(files).toEqual([["src/a.ts"]]);
|
|
71
|
+
expect(refs).toEqual([["app"]]);
|
|
72
|
+
expect(extension.logs.some((line) => line.includes("a files listener threw: boom"))).toBe(true);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
const MarksSchema = z.object({ lastUid: z.number() });
|
|
76
|
+
type Marks = z.infer<typeof MarksSchema>;
|
|
77
|
+
const parseMarks = (raw: StoredJson): Marks | undefined => MarksSchema.safeParse(raw).data;
|
|
78
|
+
|
|
79
|
+
test("a document reads an older shape through its conversions and keeps what a newer version wrote", async () => {
|
|
80
|
+
const extension = await fake({ manifest });
|
|
81
|
+
const file = join(extension.api.stateDir, "marks.json");
|
|
82
|
+
await writeFile(file, JSON.stringify({ uid: 7, addedLater: "kept" }));
|
|
83
|
+
const marks = extension.api.document("marks.json", { parse: parseMarks, fallback: () => ({ lastUid: 0 }), history: [rename("uid", "lastUid")] });
|
|
84
|
+
expect(await marks.read()).toEqual({ lastUid: 7 });
|
|
85
|
+
expect(await marks.update((current) => ({ lastUid: current.lastUid + 1 }))).toEqual({ lastUid: 8 });
|
|
86
|
+
expect(JSON.parse(await readFile(file, "utf8"))).toEqual({ lastUid: 8, addedLater: "kept" });
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
test("a document this version cannot read is left as it is rather than written over", async () => {
|
|
90
|
+
const extension = await fake({ manifest });
|
|
91
|
+
const file = join(extension.api.stateDir, "marks.json");
|
|
92
|
+
await writeFile(file, "not json");
|
|
93
|
+
const marks = extension.api.document("marks.json", { parse: parseMarks, fallback: () => ({ lastUid: 0 }) });
|
|
94
|
+
expect(await marks.read()).toEqual({ lastUid: 0 });
|
|
95
|
+
await expect(marks.update(() => ({ lastUid: 1 }))).rejects.toThrow(/cannot read/);
|
|
96
|
+
expect(await readFile(file, "utf8")).toBe("not json");
|
|
97
|
+
expect(() => extension.api.document("../escape.json", { parse: parseMarks, fallback: () => ({ lastUid: 0 }) })).toThrow(/inside the extension's state directory/);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
test("a server module is activated, served and wound down the way the backend host does it", async () => {
|
|
101
|
+
const extension = await fake({ manifest });
|
|
102
|
+
let deactivated = false;
|
|
103
|
+
await extension.activate({
|
|
104
|
+
activateServer: (api: ExtensionServerApi): ServerActivation => {
|
|
105
|
+
api.routes.mount(async (request) => (new URL(request.url).pathname === "/hello" ? Response.json({ hello: api.stateDir !== "" }) : undefined));
|
|
106
|
+
api.tools.serve(() => [{ name: "echo", description: "echoes", inputSchema: { type: "object" }, effect: "read", call: (args) => args["text"] }]);
|
|
107
|
+
return {
|
|
108
|
+
deactivate: () => {
|
|
109
|
+
deactivated = true;
|
|
110
|
+
},
|
|
111
|
+
health: () => ({ state: "degraded", detail: "the mailbox is not answering" }),
|
|
112
|
+
};
|
|
113
|
+
},
|
|
114
|
+
});
|
|
115
|
+
expect(await (await extension.route("/hello")).json()).toEqual({ hello: true });
|
|
116
|
+
expect((await extension.route("/missing")).status).toBe(404);
|
|
117
|
+
expect((await extension.tools()).map((tool) => tool.name)).toEqual(["echo"]);
|
|
118
|
+
expect(await extension.callTool("echo", { text: "hi" })).toBe("hi");
|
|
119
|
+
expect(await extension.health()).toEqual({ state: "degraded", detail: "the mailbox is not answering" });
|
|
120
|
+
await extension.deactivate();
|
|
121
|
+
expect(deactivated).toBe(true);
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
test("a health check that answers outside the vocabulary reads as degraded, saying so", async () => {
|
|
125
|
+
const extension = await fake({ manifest });
|
|
126
|
+
// SAFETY: an answer outside the vocabulary is the case under test, which the type would otherwise refuse to state.
|
|
127
|
+
await extension.activate({ activateServer: () => ({ health: () => ({ state: "fine" }) as never }) });
|
|
128
|
+
expect(await extension.health()).toEqual({ state: "degraded", detail: "its health check answered something other than ok, starting, degraded or failed" });
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
test("a process started without the daemon's environment says which variable is missing", async () => {
|
|
132
|
+
expect(() => connectExtensionProcess({})).toThrow(/missing INTENTIC_EXTENSION_ID/);
|
|
133
|
+
const root = await mkdtemp(join(tmpdir(), "process-env-"));
|
|
134
|
+
try {
|
|
135
|
+
const connected = connectExtensionProcess({
|
|
136
|
+
INTENTIC_EXTENSION_ID: "acme.probe",
|
|
137
|
+
INTENTIC_DAEMON: "http://127.0.0.1:1",
|
|
138
|
+
INTENTIC_EXTENSION_TOKEN: "t",
|
|
139
|
+
INTENTIC_WORKSPACE: root,
|
|
140
|
+
INTENTIC_EXTENSION_STATE: join(root, "state"),
|
|
141
|
+
INTENTIC_EXTENSION_CACHE: join(root, "cache"),
|
|
142
|
+
INTENTIC_EXTENSION_PERMISSIONS: JSON.stringify(["GET /ports"]),
|
|
143
|
+
});
|
|
144
|
+
expect(connected.api.stateDir).toBe(join(root, "state"));
|
|
145
|
+
expect(connected.api.workspaceRoot).toBe(root);
|
|
146
|
+
connected.close();
|
|
147
|
+
const env = {
|
|
148
|
+
INTENTIC_EXTENSION_ID: "acme.probe",
|
|
149
|
+
INTENTIC_DAEMON: "http://127.0.0.1:1",
|
|
150
|
+
INTENTIC_EXTENSION_TOKEN: "t",
|
|
151
|
+
INTENTIC_WORKSPACE: root,
|
|
152
|
+
INTENTIC_EXTENSION_STATE: root,
|
|
153
|
+
INTENTIC_EXTENSION_CACHE: root,
|
|
154
|
+
};
|
|
155
|
+
expect(() => connectExtensionProcess({ ...env, INTENTIC_EXTENSION_PERMISSIONS: "{}" })).toThrow(/not a JSON array/);
|
|
156
|
+
} finally {
|
|
157
|
+
await rm(root, { recursive: true, force: true });
|
|
158
|
+
}
|
|
159
|
+
});
|