Some checks failed
build-packages / resolve bundled mosh-client (push) Has been cancelled
build-packages / resolve bundled et-client (push) Has been cancelled
build-packages / build-macos (push) Has been cancelled
build-packages / build-windows (push) Has been cancelled
build-packages / build-linux-x64 (push) Has been cancelled
build-packages / build-linux-arm64 (push) Has been cancelled
build-packages / release (push) Has been cancelled
build-packages / update Nix release metadata (push) Has been cancelled
build-packages / bump homebrew tap (push) Has been cancelled
test / lint-and-test (push) Has been cancelled
AI automation / Route event (push) Has been cancelled
AI automation / Hand reopened issue to maintainers (push) Has been cancelled
AI automation / Clean source issue state (push) Has been cancelled
AI automation / Reconcile handoffs (push) Has been cancelled
AI automation / Classify issue (push) Has been cancelled
AI automation / Claude Code smoke (push) Has been cancelled
AI automation / Review issue follow-up (push) Has been cancelled
AI automation / Publish issue follow-up (push) Has been cancelled
AI automation / Implement with Claude Code (push) Has been cancelled
AI automation / Publish implement PR (push) Has been cancelled
AI automation / Continue queued issue comments (push) Has been cancelled
AI automation / Codex review loop (push) Has been cancelled
AI automation / Publish Codex fix (push) Has been cancelled
AI automation / Clear Codex dispatch marker (push) Has been cancelled
AI automation / Own PR re-request Codex (push) Has been cancelled
AI automation / External PR re-request Codex (push) Has been cancelled
AI automation / Poll Codex reaction / retry (push) Has been cancelled
build-et-binaries / build-linux-x64 (push) Has been cancelled
build-et-binaries / build-linux-arm64 (push) Has been cancelled
build-et-binaries / build-macos-universal (push) Has been cancelled
build-et-binaries / build-windows-x64 (push) Has been cancelled
build-et-binaries / release (push) Has been cancelled
134 lines
4.5 KiB
JavaScript
134 lines
4.5 KiB
JavaScript
"use strict";
|
|
|
|
const assert = require("node:assert/strict");
|
|
const test = require("node:test");
|
|
|
|
const {
|
|
MAX_VISIBLE_FIELD_LENGTH,
|
|
MAX_VISIBLE_RESOURCES,
|
|
createNativePermissionDecisionProvider,
|
|
describePermissionRequest,
|
|
escapePermissionText,
|
|
} = require("./nativePermissionDecision.cjs");
|
|
|
|
function permissionRequest(overrides = {}) {
|
|
return {
|
|
requestId: "request-1",
|
|
pluginId: "com.example.permissions",
|
|
pluginName: "Permissions",
|
|
publisher: "example",
|
|
permission: "filesystem.read",
|
|
resources: ["/allowed"],
|
|
resourceKinds: ["directory"],
|
|
reason: "Read project files",
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
test("native permission decisions expose every host-owned grant scope", async () => {
|
|
for (const { sessionId, response, expected } of [
|
|
{ response: 0, expected: { requestId: "request-1", decision: "deny" } },
|
|
{ response: 1, expected: { requestId: "request-1", decision: "allow", scope: "once" } },
|
|
{ sessionId: "session-1", response: 2, expected: {
|
|
requestId: "request-1", decision: "allow", scope: "session",
|
|
} },
|
|
{ response: 2, expected: {
|
|
requestId: "request-1", decision: "allow", scope: "application",
|
|
} },
|
|
{ response: 3, expected: {
|
|
requestId: "request-1", decision: "allow", scope: "always",
|
|
} },
|
|
]) {
|
|
let captured;
|
|
const signal = new AbortController().signal;
|
|
const provider = createNativePermissionDecisionProvider({
|
|
dialog: {
|
|
async showMessageBox(_window, options) {
|
|
captured = options;
|
|
return { response };
|
|
},
|
|
},
|
|
window: {},
|
|
});
|
|
assert.deepEqual(await provider(permissionRequest({ sessionId }), { signal }), expected);
|
|
assert.equal(captured.signal, signal);
|
|
assert.equal(captured.cancelId, 0);
|
|
assert.match(captured.detail, /directory and descendants/u);
|
|
}
|
|
});
|
|
|
|
test("native permission decisions fail closed on cancellation and abort", async () => {
|
|
const provider = createNativePermissionDecisionProvider({
|
|
dialog: { showMessageBox: async () => ({ response: 999 }) },
|
|
});
|
|
assert.deepEqual(await provider(permissionRequest()), {
|
|
requestId: "request-1",
|
|
decision: "deny",
|
|
});
|
|
const controller = new AbortController();
|
|
controller.abort(new Error("runtime stopped"));
|
|
await assert.rejects(provider(permissionRequest(), { signal: controller.signal }), /runtime stopped/);
|
|
});
|
|
|
|
test("terminal interceptor permission prompts omit the unsupported one-use choice", async () => {
|
|
let captured;
|
|
const provider = createNativePermissionDecisionProvider({
|
|
dialog: {
|
|
async showMessageBox(options) {
|
|
captured = options;
|
|
return { response: 1 };
|
|
},
|
|
},
|
|
});
|
|
assert.deepEqual(await provider(permissionRequest({
|
|
sessionId: "session-1",
|
|
operationId: `terminal.interceptor.input:com.example.${"x".repeat(170)}`,
|
|
allowedScopes: ["session", "application", "always"],
|
|
})), {
|
|
requestId: "request-1",
|
|
decision: "allow",
|
|
scope: "session",
|
|
});
|
|
assert.deepEqual(captured.buttons, [
|
|
"Deny",
|
|
"Allow for Session",
|
|
"Allow for Application",
|
|
"Always Allow",
|
|
]);
|
|
});
|
|
|
|
test("native permission request details are bounded and preserve resource kinds", () => {
|
|
const resources = Array.from({ length: MAX_VISIBLE_RESOURCES + 5 }, (_, index) => `/path/${index}`);
|
|
const detail = describePermissionRequest(permissionRequest({
|
|
resources,
|
|
resourceKinds: resources.map(() => "exact"),
|
|
}));
|
|
assert.match(detail, /and 5 more/u);
|
|
assert.doesNotMatch(detail, /\/path\/24/u);
|
|
});
|
|
|
|
test("native permission dialogs visibly escape control and bidi characters", async () => {
|
|
let captured;
|
|
const provider = createNativePermissionDecisionProvider({
|
|
dialog: {
|
|
async showMessageBox(options) {
|
|
captured = options;
|
|
return { response: 0 };
|
|
},
|
|
},
|
|
});
|
|
await provider(permissionRequest({
|
|
pluginName: "Plugin\nPermission: vault.credentials",
|
|
publisher: "publisher\u202e",
|
|
reason: "Read this\r\nAlways Allow",
|
|
resources: ["/safe\nPermission: runtime.advanced"],
|
|
resourceKinds: ["exact"],
|
|
}));
|
|
assert.match(captured.message, /Plugin\\nPermission: vault\.credentials/u);
|
|
assert.match(captured.detail, /publisher\\u202e/u);
|
|
assert.match(captured.detail, /Read this\\r\\nAlways Allow/u);
|
|
assert.match(captured.detail, /\/safe\\nPermission: runtime\.advanced/u);
|
|
assert.equal(captured.detail.split("\n").filter((line) => line.startsWith("Permission:")).length, 1);
|
|
assert.equal(escapePermissionText("x".repeat(MAX_VISIBLE_FIELD_LENGTH + 20)).length, MAX_VISIBLE_FIELD_LENGTH);
|
|
});
|