pinokiod 7.5.21 → 7.5.24
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/kernel/api/index.js +20 -8
- package/kernel/plugin_sources.js +8 -0
- package/package.json +1 -1
- package/server/index.js +113 -13
- package/server/lib/plugin_delete.js +89 -0
- package/server/public/common.js +6 -2
- package/server/public/plugin-detail.js +131 -1
- package/server/public/task-launcher.css +221 -17
- package/server/public/universal-launcher.js +2 -1
- package/server/socket.js +59 -99
- package/server/views/app.ejs +141 -20
- package/server/views/plugin_detail.ejs +52 -20
- package/server/views/plugins.ejs +14 -1
- package/server/views/terminal.ejs +11 -5
- package/test/launch-requirements.test.js +27 -0
- package/test/plugin-delete.test.js +143 -0
- package/test/plugin-detail-ui.test.js +269 -0
- package/test/plugin-detail-view.test.js +160 -0
- package/test/plugin-sources.test.js +19 -0
- package/test/terminal-log-paths.test.js +238 -0
- package/test/universal-launcher.smoke.spec.js +50 -0
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
const assert = require("node:assert/strict")
|
|
2
|
+
const fs = require("node:fs/promises")
|
|
3
|
+
const os = require("node:os")
|
|
4
|
+
const path = require("node:path")
|
|
5
|
+
const test = require("node:test")
|
|
6
|
+
const { deletePluginFolder, resolvePluginDeleteTarget } = require("../server/lib/plugin_delete")
|
|
7
|
+
|
|
8
|
+
async function withHome(fn) {
|
|
9
|
+
const homedir = await fs.mkdtemp(path.join(os.tmpdir(), "pinokio-plugin-delete-"))
|
|
10
|
+
const kernel = {
|
|
11
|
+
homedir,
|
|
12
|
+
path: (...parts) => path.join(homedir, ...parts),
|
|
13
|
+
}
|
|
14
|
+
try {
|
|
15
|
+
return await fn({ homedir, kernel })
|
|
16
|
+
} finally {
|
|
17
|
+
await fs.rm(homedir, { recursive: true, force: true })
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
test("deletePluginFolder removes only the selected downloaded plugin folder", async () => {
|
|
22
|
+
await withHome(async ({ homedir, kernel }) => {
|
|
23
|
+
const demoDir = path.join(homedir, "plugin", "demo")
|
|
24
|
+
const otherDir = path.join(homedir, "plugin", "other")
|
|
25
|
+
await fs.mkdir(demoDir, { recursive: true })
|
|
26
|
+
await fs.mkdir(otherDir, { recursive: true })
|
|
27
|
+
await fs.writeFile(path.join(demoDir, "pinokio.js"), "module.exports = { run: [] }\n")
|
|
28
|
+
await fs.writeFile(path.join(demoDir, "notes.txt"), "demo\n")
|
|
29
|
+
await fs.writeFile(path.join(otherDir, "pinokio.js"), "module.exports = { run: [] }\n")
|
|
30
|
+
|
|
31
|
+
const target = await deletePluginFolder({
|
|
32
|
+
kernel,
|
|
33
|
+
plugin: {
|
|
34
|
+
pluginPath: "/plugin/demo/pinokio.js",
|
|
35
|
+
source: "local",
|
|
36
|
+
},
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
assert.equal(target.localLabel, "plugin/demo")
|
|
40
|
+
await assert.rejects(fs.stat(demoDir), /ENOENT/)
|
|
41
|
+
assert.ok((await fs.stat(path.join(homedir, "plugin"))).isDirectory())
|
|
42
|
+
assert.ok((await fs.stat(path.join(otherDir, "pinokio.js"))).isFile())
|
|
43
|
+
})
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
test("resolvePluginDeleteTarget rejects built-in system plugins", async () => {
|
|
47
|
+
await withHome(async ({ kernel }) => {
|
|
48
|
+
assert.throws(
|
|
49
|
+
() => resolvePluginDeleteTarget({
|
|
50
|
+
kernel,
|
|
51
|
+
plugin: {
|
|
52
|
+
pluginPath: "/pinokio/run/plugin/demo/pinokio.js",
|
|
53
|
+
source: "system",
|
|
54
|
+
system: true,
|
|
55
|
+
},
|
|
56
|
+
}),
|
|
57
|
+
/Built-in plugins cannot be deleted/
|
|
58
|
+
)
|
|
59
|
+
})
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
test("resolvePluginDeleteTarget rejects traversal out of downloaded plugin folders", async () => {
|
|
63
|
+
await withHome(async ({ kernel }) => {
|
|
64
|
+
assert.throws(
|
|
65
|
+
() => resolvePluginDeleteTarget({
|
|
66
|
+
kernel,
|
|
67
|
+
plugin: {
|
|
68
|
+
pluginPath: "/plugin/../api/demo/pinokio.js",
|
|
69
|
+
source: "local",
|
|
70
|
+
},
|
|
71
|
+
}),
|
|
72
|
+
/(Only downloaded plugin folders can be deleted|outside the downloaded plugin folder)/
|
|
73
|
+
)
|
|
74
|
+
})
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
test("resolvePluginDeleteTarget refuses to delete the plugin root", async () => {
|
|
78
|
+
await withHome(async ({ homedir, kernel }) => {
|
|
79
|
+
await fs.mkdir(path.join(homedir, "plugin"), { recursive: true })
|
|
80
|
+
|
|
81
|
+
assert.throws(
|
|
82
|
+
() => resolvePluginDeleteTarget({
|
|
83
|
+
kernel,
|
|
84
|
+
plugin: {
|
|
85
|
+
pluginPath: "/plugin/pinokio.js",
|
|
86
|
+
source: "local",
|
|
87
|
+
},
|
|
88
|
+
}),
|
|
89
|
+
/plugin root folder cannot be deleted/
|
|
90
|
+
)
|
|
91
|
+
})
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
test("deletePluginFolder rejects plugin directory symlinks that resolve outside the plugin root", async () => {
|
|
95
|
+
await withHome(async ({ homedir, kernel }) => {
|
|
96
|
+
const outsideDir = await fs.mkdtemp(path.join(os.tmpdir(), "pinokio-plugin-delete-outside-"))
|
|
97
|
+
try {
|
|
98
|
+
await fs.mkdir(path.join(homedir, "plugin"), { recursive: true })
|
|
99
|
+
await fs.writeFile(path.join(outsideDir, "pinokio.js"), "module.exports = { run: [] }\n")
|
|
100
|
+
await fs.symlink(outsideDir, path.join(homedir, "plugin", "linked"), "dir")
|
|
101
|
+
|
|
102
|
+
await assert.rejects(
|
|
103
|
+
deletePluginFolder({
|
|
104
|
+
kernel,
|
|
105
|
+
plugin: {
|
|
106
|
+
pluginPath: "/plugin/linked/pinokio.js",
|
|
107
|
+
source: "local",
|
|
108
|
+
},
|
|
109
|
+
}),
|
|
110
|
+
/outside the downloaded plugin folder/
|
|
111
|
+
)
|
|
112
|
+
assert.ok((await fs.stat(path.join(outsideDir, "pinokio.js"))).isFile())
|
|
113
|
+
} finally {
|
|
114
|
+
await fs.rm(outsideDir, { recursive: true, force: true })
|
|
115
|
+
}
|
|
116
|
+
})
|
|
117
|
+
})
|
|
118
|
+
|
|
119
|
+
test("deletePluginFolder removes nested symlinks without following them outside the plugin folder", async () => {
|
|
120
|
+
await withHome(async ({ homedir, kernel }) => {
|
|
121
|
+
const outsideDir = await fs.mkdtemp(path.join(os.tmpdir(), "pinokio-plugin-delete-outside-"))
|
|
122
|
+
try {
|
|
123
|
+
const pluginDir = path.join(homedir, "plugin", "demo")
|
|
124
|
+
await fs.mkdir(pluginDir, { recursive: true })
|
|
125
|
+
await fs.writeFile(path.join(pluginDir, "pinokio.js"), "module.exports = { run: [] }\n")
|
|
126
|
+
await fs.writeFile(path.join(outsideDir, "keep.txt"), "keep\n")
|
|
127
|
+
await fs.symlink(outsideDir, path.join(pluginDir, "outside-link"), "dir")
|
|
128
|
+
|
|
129
|
+
await deletePluginFolder({
|
|
130
|
+
kernel,
|
|
131
|
+
plugin: {
|
|
132
|
+
pluginPath: "/plugin/demo/pinokio.js",
|
|
133
|
+
source: "local",
|
|
134
|
+
},
|
|
135
|
+
})
|
|
136
|
+
|
|
137
|
+
await assert.rejects(fs.stat(pluginDir), /ENOENT/)
|
|
138
|
+
assert.ok((await fs.stat(path.join(outsideDir, "keep.txt"))).isFile())
|
|
139
|
+
} finally {
|
|
140
|
+
await fs.rm(outsideDir, { recursive: true, force: true })
|
|
141
|
+
}
|
|
142
|
+
})
|
|
143
|
+
})
|
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
const assert = require("node:assert/strict")
|
|
2
|
+
const fs = require("node:fs/promises")
|
|
3
|
+
const path = require("node:path")
|
|
4
|
+
const test = require("node:test")
|
|
5
|
+
const { JSDOM, VirtualConsole } = require("jsdom")
|
|
6
|
+
|
|
7
|
+
const repoRoot = path.resolve(__dirname, "..")
|
|
8
|
+
|
|
9
|
+
function visibleTextMatches(document, selector, text) {
|
|
10
|
+
return Array.from(document.querySelectorAll(selector)).filter((element) => {
|
|
11
|
+
if (element.textContent.trim() !== text) {
|
|
12
|
+
return false
|
|
13
|
+
}
|
|
14
|
+
return !element.closest(".task-hidden")
|
|
15
|
+
})
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
async function renderSharePanel(shareOverrides, options = {}) {
|
|
19
|
+
const script = await fs.readFile(path.join(repoRoot, "server/public/plugin-detail.js"), "utf8")
|
|
20
|
+
const share = {
|
|
21
|
+
manageable: true,
|
|
22
|
+
githubConnected: false,
|
|
23
|
+
gitInitialized: false,
|
|
24
|
+
hasCommit: false,
|
|
25
|
+
remoteUrl: "",
|
|
26
|
+
remoteWebUrl: "",
|
|
27
|
+
hasPublished: false,
|
|
28
|
+
changeCount: 0,
|
|
29
|
+
aheadCount: 0,
|
|
30
|
+
createUrl: "/github/create",
|
|
31
|
+
commitUrl: "/github/commit",
|
|
32
|
+
pushUrl: "/github/push",
|
|
33
|
+
dir: "/tmp/demo-plugin",
|
|
34
|
+
...shareOverrides,
|
|
35
|
+
}
|
|
36
|
+
const staticClass = share.remoteWebUrl || (share.remoteUrl && share.githubConnected) ? "" : " task-hidden"
|
|
37
|
+
const remoteClass = share.remoteWebUrl ? "" : " task-hidden"
|
|
38
|
+
const refreshClass = share.remoteUrl && share.githubConnected ? "" : " task-hidden"
|
|
39
|
+
const bootstrap = {
|
|
40
|
+
plugin: { title: "Demo Plugin", cwd: "/tmp/demo-plugin", pluginPath: "plugin/demo/pinokio.js" },
|
|
41
|
+
share,
|
|
42
|
+
apps: [],
|
|
43
|
+
stateUrl: "",
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const dom = new JSDOM(`<!doctype html>
|
|
47
|
+
<body>
|
|
48
|
+
${options.actionMarkup || ""}
|
|
49
|
+
<section class="task-side-group">
|
|
50
|
+
<div class="task-section-note" data-plugin-share-note></div>
|
|
51
|
+
<div class="task-share-next-step-main">
|
|
52
|
+
<h3 data-plugin-share-next-title></h3>
|
|
53
|
+
<p data-plugin-share-next-copy></p>
|
|
54
|
+
<div class="task-actions" data-plugin-share-next-actions></div>
|
|
55
|
+
</div>
|
|
56
|
+
<form class="task-share-inline-create task-hidden" data-plugin-share-create-form>
|
|
57
|
+
<input type="text" data-plugin-share-repo-name>
|
|
58
|
+
<select data-plugin-share-visibility><option value="public">Public</option></select>
|
|
59
|
+
<button type="button" data-plugin-share-create-cancel>Cancel</button>
|
|
60
|
+
</form>
|
|
61
|
+
<div class="task-actions plugin-share-static-actions${staticClass}" data-plugin-share-static-actions>
|
|
62
|
+
<a class="task-link-button${remoteClass}" href="${share.remoteWebUrl || "#"}" data-plugin-share-remote-link>
|
|
63
|
+
<span>View on GitHub</span>
|
|
64
|
+
</a>
|
|
65
|
+
<button class="${refreshClass}" type="button" data-plugin-share-refresh><span>Check again</span></button>
|
|
66
|
+
</div>
|
|
67
|
+
<p data-plugin-share-copy></p>
|
|
68
|
+
<div data-plugin-share-feedback hidden></div>
|
|
69
|
+
</section>
|
|
70
|
+
<script id="plugin-detail-bootstrap" type="application/json">${JSON.stringify(bootstrap)}</script>
|
|
71
|
+
</body>`, {
|
|
72
|
+
runScripts: "dangerously",
|
|
73
|
+
url: "http://localhost/plugin",
|
|
74
|
+
virtualConsole: options.virtualConsole,
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
if (typeof options.beforeScript === "function") {
|
|
78
|
+
options.beforeScript(dom.window)
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const scriptElement = dom.window.document.createElement("script")
|
|
82
|
+
scriptElement.textContent = script
|
|
83
|
+
dom.window.document.body.appendChild(scriptElement)
|
|
84
|
+
|
|
85
|
+
return dom.window.document
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
test("plugin detail hides static GitHub actions until a remote exists", async () => {
|
|
89
|
+
const document = await renderSharePanel()
|
|
90
|
+
const staticActions = document.querySelector("[data-plugin-share-static-actions]")
|
|
91
|
+
|
|
92
|
+
assert.ok(staticActions.classList.contains("task-hidden"))
|
|
93
|
+
assert.equal(visibleTextMatches(document, "a,button", "Check again").length, 1)
|
|
94
|
+
assert.equal(document.querySelector("[data-plugin-share-note]").textContent, "Setup needed")
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
test("plugin detail shows one static refresh action after a remote exists", async () => {
|
|
98
|
+
const document = await renderSharePanel({
|
|
99
|
+
githubConnected: true,
|
|
100
|
+
gitInitialized: true,
|
|
101
|
+
hasCommit: true,
|
|
102
|
+
remoteUrl: "git@github.com:octocat/demo-plugin.git",
|
|
103
|
+
remoteWebUrl: "https://github.com/octocat/demo-plugin",
|
|
104
|
+
hasPublished: true,
|
|
105
|
+
})
|
|
106
|
+
const staticActions = document.querySelector("[data-plugin-share-static-actions]")
|
|
107
|
+
|
|
108
|
+
assert.ok(!staticActions.classList.contains("task-hidden"))
|
|
109
|
+
assert.equal(visibleTextMatches(document, "a,button", "Check again").length, 1)
|
|
110
|
+
assert.equal(visibleTextMatches(document, "a,button", "View on GitHub").length, 1)
|
|
111
|
+
assert.equal(document.querySelector("[data-plugin-share-note]").textContent, "Connected")
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
test("plugin detail keeps refresh available for non-GitHub remotes", async () => {
|
|
115
|
+
const document = await renderSharePanel({
|
|
116
|
+
githubConnected: true,
|
|
117
|
+
gitInitialized: true,
|
|
118
|
+
hasCommit: true,
|
|
119
|
+
remoteUrl: "https://gitlab.com/octocat/demo-plugin.git",
|
|
120
|
+
remoteWebUrl: "",
|
|
121
|
+
hasPublished: true,
|
|
122
|
+
})
|
|
123
|
+
const staticActions = document.querySelector("[data-plugin-share-static-actions]")
|
|
124
|
+
|
|
125
|
+
assert.ok(!staticActions.classList.contains("task-hidden"))
|
|
126
|
+
assert.equal(visibleTextMatches(document, "a,button", "Check again").length, 1)
|
|
127
|
+
assert.equal(visibleTextMatches(document, "a,button", "View on GitHub").length, 0)
|
|
128
|
+
})
|
|
129
|
+
|
|
130
|
+
test("plugin detail does not duplicate refresh when auth is disconnected but a remote exists", async () => {
|
|
131
|
+
const document = await renderSharePanel({
|
|
132
|
+
githubConnected: false,
|
|
133
|
+
gitInitialized: true,
|
|
134
|
+
hasCommit: true,
|
|
135
|
+
remoteUrl: "git@github.com:octocat/demo-plugin.git",
|
|
136
|
+
remoteWebUrl: "https://github.com/octocat/demo-plugin",
|
|
137
|
+
hasPublished: true,
|
|
138
|
+
})
|
|
139
|
+
|
|
140
|
+
assert.equal(visibleTextMatches(document, "a,button", "Check again").length, 1)
|
|
141
|
+
assert.equal(visibleTextMatches(document, "a,button", "View on GitHub").length, 1)
|
|
142
|
+
assert.equal(document.querySelector("[data-plugin-share-note]").textContent, "Connected")
|
|
143
|
+
})
|
|
144
|
+
|
|
145
|
+
test("plugin action event panel completion closes the modal and refreshes after close", async () => {
|
|
146
|
+
let closeCount = 0
|
|
147
|
+
let fireOptions = null
|
|
148
|
+
let actionIframe = null
|
|
149
|
+
let reloadCount = 0
|
|
150
|
+
const unexpectedJsdomErrors = []
|
|
151
|
+
const virtualConsole = new VirtualConsole()
|
|
152
|
+
virtualConsole.on("jsdomError", (error) => {
|
|
153
|
+
if (error && /Not implemented: navigation/.test(error.message || "")) {
|
|
154
|
+
reloadCount += 1
|
|
155
|
+
return
|
|
156
|
+
}
|
|
157
|
+
unexpectedJsdomErrors.push(error)
|
|
158
|
+
})
|
|
159
|
+
const document = await renderSharePanel({}, {
|
|
160
|
+
actionMarkup: '<button type="button" data-plugin-action="install">Install</button>',
|
|
161
|
+
virtualConsole,
|
|
162
|
+
beforeScript(window) {
|
|
163
|
+
window.Swal = {
|
|
164
|
+
fire(options) {
|
|
165
|
+
fireOptions = options
|
|
166
|
+
const popup = window.document.createElement("div")
|
|
167
|
+
popup.innerHTML = options.html || ""
|
|
168
|
+
window.document.body.appendChild(popup)
|
|
169
|
+
actionIframe = popup.querySelector("iframe")
|
|
170
|
+
if (typeof options.didOpen === "function") {
|
|
171
|
+
options.didOpen(popup)
|
|
172
|
+
}
|
|
173
|
+
return Promise.resolve({})
|
|
174
|
+
},
|
|
175
|
+
close() {
|
|
176
|
+
closeCount += 1
|
|
177
|
+
if (fireOptions && typeof fireOptions.didClose === "function") {
|
|
178
|
+
fireOptions.didClose()
|
|
179
|
+
}
|
|
180
|
+
},
|
|
181
|
+
}
|
|
182
|
+
},
|
|
183
|
+
})
|
|
184
|
+
const window = document.defaultView
|
|
185
|
+
|
|
186
|
+
document.querySelector("[data-plugin-action='install']").click()
|
|
187
|
+
assert.ok(fireOptions)
|
|
188
|
+
assert.match(fireOptions.html, /\/action\/install\/plugin\/demo\/pinokio\.js/)
|
|
189
|
+
assert.match(fireOptions.html, /__pinokio_event_panel=1/)
|
|
190
|
+
assert.ok(actionIframe && actionIframe.contentWindow)
|
|
191
|
+
|
|
192
|
+
window.dispatchEvent(new window.MessageEvent("message", {
|
|
193
|
+
origin: window.location.origin,
|
|
194
|
+
data: {
|
|
195
|
+
e: "pinokio:event-panel-status",
|
|
196
|
+
success: true,
|
|
197
|
+
},
|
|
198
|
+
}))
|
|
199
|
+
assert.equal(closeCount, 0)
|
|
200
|
+
assert.equal(reloadCount, 0)
|
|
201
|
+
|
|
202
|
+
window.dispatchEvent(new window.MessageEvent("message", {
|
|
203
|
+
origin: window.location.origin,
|
|
204
|
+
source: actionIframe.contentWindow,
|
|
205
|
+
data: {
|
|
206
|
+
e: "pinokio:event-panel-status",
|
|
207
|
+
success: true,
|
|
208
|
+
},
|
|
209
|
+
}))
|
|
210
|
+
|
|
211
|
+
assert.equal(closeCount, 1)
|
|
212
|
+
assert.equal(reloadCount, 1)
|
|
213
|
+
assert.deepEqual(unexpectedJsdomErrors, [])
|
|
214
|
+
})
|
|
215
|
+
|
|
216
|
+
test("plugin delete confirmation and failure do not request SweetAlert icons", async () => {
|
|
217
|
+
const fireOptions = []
|
|
218
|
+
const document = await renderSharePanel({
|
|
219
|
+
localLabel: "plugin/demo",
|
|
220
|
+
}, {
|
|
221
|
+
actionMarkup: '<button type="button" data-plugin-delete>Delete</button>',
|
|
222
|
+
beforeScript(window) {
|
|
223
|
+
window.fetch = () => Promise.resolve({
|
|
224
|
+
ok: false,
|
|
225
|
+
status: 400,
|
|
226
|
+
json: () => Promise.resolve({ ok: false, error: "Plugin path is required." }),
|
|
227
|
+
})
|
|
228
|
+
window.Swal = {
|
|
229
|
+
fire(options) {
|
|
230
|
+
fireOptions.push(options)
|
|
231
|
+
return Promise.resolve(fireOptions.length === 1 ? { isConfirmed: true } : {})
|
|
232
|
+
},
|
|
233
|
+
}
|
|
234
|
+
},
|
|
235
|
+
})
|
|
236
|
+
|
|
237
|
+
document.querySelector("[data-plugin-delete]").click()
|
|
238
|
+
await new Promise((resolve) => setTimeout(resolve, 0))
|
|
239
|
+
await new Promise((resolve) => setTimeout(resolve, 0))
|
|
240
|
+
|
|
241
|
+
assert.equal(fireOptions.length, 2)
|
|
242
|
+
assert.equal(fireOptions[0].icon, undefined)
|
|
243
|
+
assert.equal(fireOptions[0].title, "Delete Demo Plugin?")
|
|
244
|
+
assert.match(fireOptions[0].html, /plugin\/demo/)
|
|
245
|
+
assert.equal(fireOptions[0].confirmButtonText, "Delete plugin")
|
|
246
|
+
assert.equal(fireOptions[1].icon, undefined)
|
|
247
|
+
assert.equal(fireOptions[1].title, "Delete failed")
|
|
248
|
+
assert.equal(fireOptions[1].text, "Plugin path is required.")
|
|
249
|
+
assert.equal(fireOptions[1].confirmButtonText, "Close")
|
|
250
|
+
assert.equal(fireOptions[1].buttonsStyling, false)
|
|
251
|
+
})
|
|
252
|
+
|
|
253
|
+
test("terminal action view uses generic event panel completion", async () => {
|
|
254
|
+
const terminalView = await fs.readFile(path.join(repoRoot, "server/views/terminal.ejs"), "utf8")
|
|
255
|
+
const disconnectBranchStart = terminalView.indexOf("packet.type === 'disconnect'")
|
|
256
|
+
const eventBranchStart = terminalView.indexOf('packet.type === "event"', disconnectBranchStart)
|
|
257
|
+
const eventBranchEnd = terminalView.indexOf("//this.socket.close()", eventBranchStart)
|
|
258
|
+
|
|
259
|
+
assert.ok(disconnectBranchStart >= 0)
|
|
260
|
+
assert.ok(eventBranchStart > disconnectBranchStart)
|
|
261
|
+
assert.ok(eventBranchEnd > eventBranchStart)
|
|
262
|
+
|
|
263
|
+
assert.match(terminalView, /pinokio:event-panel-status/)
|
|
264
|
+
assert.match(terminalView.slice(disconnectBranchStart, eventBranchStart), /pinokio:event-panel-status/)
|
|
265
|
+
assert.match(terminalView.slice(eventBranchStart, eventBranchEnd), /eventPanelStopPending = true/)
|
|
266
|
+
assert.doesNotMatch(terminalView.slice(eventBranchStart, eventBranchEnd), /pinokio:event-panel-status/)
|
|
267
|
+
assert.doesNotMatch(terminalView, /pluginActionCompletion/)
|
|
268
|
+
assert.doesNotMatch(terminalView, /pinokio:plugin-action-complete/)
|
|
269
|
+
})
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
const assert = require("node:assert/strict")
|
|
2
|
+
const path = require("node:path")
|
|
3
|
+
const test = require("node:test")
|
|
4
|
+
const ejs = require("ejs")
|
|
5
|
+
const { JSDOM } = require("jsdom")
|
|
6
|
+
|
|
7
|
+
const viewPath = path.resolve(__dirname, "..", "server/views/plugin_detail.ejs")
|
|
8
|
+
|
|
9
|
+
function basePlugin(overrides = {}) {
|
|
10
|
+
return {
|
|
11
|
+
title: "Demo Plugin",
|
|
12
|
+
description: "Demo plugin description.",
|
|
13
|
+
link: "https://example.com/plugin",
|
|
14
|
+
image: "",
|
|
15
|
+
icon: "fa-solid fa-terminal",
|
|
16
|
+
pluginPath: "/plugin/demo/pinokio.js",
|
|
17
|
+
extraParams: [],
|
|
18
|
+
defaultCwd: "",
|
|
19
|
+
hasInstall: true,
|
|
20
|
+
hasUpdate: true,
|
|
21
|
+
hasUninstall: true,
|
|
22
|
+
hasInstalledCheck: true,
|
|
23
|
+
installed: true,
|
|
24
|
+
category: "cli",
|
|
25
|
+
...overrides,
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function basePluginUi(overrides = {}) {
|
|
30
|
+
return {
|
|
31
|
+
badges: [{ label: "Local plugin", tone: "accent" }],
|
|
32
|
+
launchSummary: "Launches inside Pinokio",
|
|
33
|
+
hasChanges: false,
|
|
34
|
+
changePreview: [],
|
|
35
|
+
extraChangeCount: 0,
|
|
36
|
+
localChangesCopy: "",
|
|
37
|
+
showSidebar: true,
|
|
38
|
+
sourceLabel: "Local folder",
|
|
39
|
+
sourceValue: "plugin/demo",
|
|
40
|
+
statusLabel: "Status",
|
|
41
|
+
statusValue: "Not version tracked yet",
|
|
42
|
+
canOpenFolder: false,
|
|
43
|
+
canManageSource: true,
|
|
44
|
+
isManagedSource: false,
|
|
45
|
+
githubPanelTitle: "GitHub",
|
|
46
|
+
githubPanelCopy: "",
|
|
47
|
+
remoteLabel: "",
|
|
48
|
+
...overrides,
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function baseShareState(overrides = {}) {
|
|
53
|
+
return {
|
|
54
|
+
ownership: "local",
|
|
55
|
+
manageable: true,
|
|
56
|
+
canOpenFolder: false,
|
|
57
|
+
dir: "/tmp/pinokio/plugin/demo",
|
|
58
|
+
localLabel: "plugin/demo",
|
|
59
|
+
remoteUrl: "",
|
|
60
|
+
remoteWebUrl: "",
|
|
61
|
+
githubConnected: false,
|
|
62
|
+
gitInitialized: false,
|
|
63
|
+
hasCommit: false,
|
|
64
|
+
changeCount: 0,
|
|
65
|
+
changes: [],
|
|
66
|
+
branch: "HEAD",
|
|
67
|
+
commitUrl: "",
|
|
68
|
+
createUrl: "",
|
|
69
|
+
pushUrl: "",
|
|
70
|
+
...overrides,
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
async function renderPluginDetail({ plugin = {}, pluginUi = {}, shareState = {} } = {}) {
|
|
75
|
+
const html = await ejs.renderFile(viewPath, {
|
|
76
|
+
theme: "dark",
|
|
77
|
+
agent: "browser",
|
|
78
|
+
list: [],
|
|
79
|
+
current_host: "",
|
|
80
|
+
showPeerAccess: false,
|
|
81
|
+
plugin: basePlugin(plugin),
|
|
82
|
+
pluginUi: basePluginUi(pluginUi),
|
|
83
|
+
pluginCwd: "",
|
|
84
|
+
shareState: baseShareState(shareState),
|
|
85
|
+
apps: [],
|
|
86
|
+
})
|
|
87
|
+
return new JSDOM(html).window.document
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
test("plugin detail omits the right column for built-in system plugins", async () => {
|
|
91
|
+
const document = await renderPluginDetail({
|
|
92
|
+
plugin: {
|
|
93
|
+
title: "Antigravity CLI Auto",
|
|
94
|
+
pluginPath: "/pinokio/run/plugin/antigravity-cli-auto/pinokio.js",
|
|
95
|
+
},
|
|
96
|
+
pluginUi: {
|
|
97
|
+
badges: [{ label: "Built-in plugin", tone: "neutral" }],
|
|
98
|
+
showSidebar: false,
|
|
99
|
+
},
|
|
100
|
+
shareState: {
|
|
101
|
+
ownership: "system",
|
|
102
|
+
localLabel: "plugin/antigravity-cli-auto/pinokio.js",
|
|
103
|
+
manageable: false,
|
|
104
|
+
},
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
assert.equal(document.querySelector(".task-detail-sidebar"), null)
|
|
108
|
+
assert.ok(document.querySelector(".task-detail-layout-single"))
|
|
109
|
+
assert.equal(document.querySelector("[data-plugin-share-next-title]"), null)
|
|
110
|
+
assert.doesNotMatch(document.body.textContent, /Plugin status/)
|
|
111
|
+
assert.match(document.querySelector(".task-header-actions").textContent, /Website/)
|
|
112
|
+
assert.doesNotMatch(document.querySelector(".task-header-actions").textContent, /Back/)
|
|
113
|
+
assert.equal(document.querySelector("[data-plugin-delete]"), null)
|
|
114
|
+
assert.equal(document.querySelector('.plugin-detail-breadcrumb a[href="/plugins"]')?.textContent, "Plugins")
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
test("plugin detail keeps source management column for local plugins", async () => {
|
|
118
|
+
const document = await renderPluginDetail()
|
|
119
|
+
|
|
120
|
+
assert.ok(document.querySelector(".task-detail-sidebar"))
|
|
121
|
+
assert.equal(document.querySelector(".task-detail-layout-single"), null)
|
|
122
|
+
assert.ok(document.querySelector("[data-plugin-share-next-title]"))
|
|
123
|
+
assert.match(document.body.textContent, /Plugin status/)
|
|
124
|
+
assert.equal(document.querySelector('.plugin-detail-breadcrumb a[href="/plugins"]')?.textContent, "Plugins")
|
|
125
|
+
assert.equal(
|
|
126
|
+
document.querySelector('.plugin-detail-breadcrumb a[aria-current="page"]')?.getAttribute("href"),
|
|
127
|
+
"/plugin?path=%2Fplugin%2Fdemo%2Fpinokio.js"
|
|
128
|
+
)
|
|
129
|
+
assert.match(document.querySelector(".task-header-actions").textContent, /Delete/)
|
|
130
|
+
assert.ok(document.querySelector(".task-header-actions [data-plugin-delete]"))
|
|
131
|
+
assert.doesNotMatch(document.querySelector(".task-header-actions").textContent, /Back/)
|
|
132
|
+
assert.equal(document.querySelectorAll(".plugin-detail-status-row").length, 3)
|
|
133
|
+
assert.equal(document.querySelectorAll(".task-detail-sidebar .task-meta-chip").length, 0)
|
|
134
|
+
assert.equal(
|
|
135
|
+
Array.from(document.querySelectorAll(".task-detail-sidebar h2")).some((heading) => heading.textContent.trim() === "GitHub"),
|
|
136
|
+
false
|
|
137
|
+
)
|
|
138
|
+
})
|
|
139
|
+
|
|
140
|
+
test("plugin detail does not show delete for managed non-local plugin source", async () => {
|
|
141
|
+
const document = await renderPluginDetail({
|
|
142
|
+
pluginUi: {
|
|
143
|
+
badges: [{ label: "Managed by Pinokio", tone: "neutral" }],
|
|
144
|
+
canManageSource: false,
|
|
145
|
+
isManagedSource: true,
|
|
146
|
+
sourceLabel: "Managed folder",
|
|
147
|
+
sourceValue: "plugin/code/demo",
|
|
148
|
+
githubPanelTitle: "Managed by Pinokio",
|
|
149
|
+
githubPanelCopy: "Managed plugin source.",
|
|
150
|
+
},
|
|
151
|
+
shareState: {
|
|
152
|
+
ownership: "managed",
|
|
153
|
+
manageable: false,
|
|
154
|
+
dir: "/tmp/pinokio/plugin/code/demo",
|
|
155
|
+
localLabel: "plugin/code/demo",
|
|
156
|
+
},
|
|
157
|
+
})
|
|
158
|
+
|
|
159
|
+
assert.equal(document.querySelector("[data-plugin-delete]"), null)
|
|
160
|
+
})
|
|
@@ -32,6 +32,25 @@ test("resolveLauncherPluginSelection returns app-dev plugin query paths", () =>
|
|
|
32
32
|
)
|
|
33
33
|
})
|
|
34
34
|
|
|
35
|
+
test("systemPluginPathForLocalPath maps local-shaped plugin paths to system candidates", () => {
|
|
36
|
+
assert.strictEqual(
|
|
37
|
+
PluginSources.systemPluginPathForLocalPath("/plugin/antigravity-cli-auto/pinokio.js"),
|
|
38
|
+
"/pinokio/run/plugin/antigravity-cli-auto/pinokio.js"
|
|
39
|
+
)
|
|
40
|
+
assert.strictEqual(
|
|
41
|
+
PluginSources.systemPluginPathForLocalPath("plugin/antigravity-cli-auto"),
|
|
42
|
+
"/pinokio/run/plugin/antigravity-cli-auto/pinokio.js"
|
|
43
|
+
)
|
|
44
|
+
assert.strictEqual(
|
|
45
|
+
PluginSources.systemPluginPathForLocalPath("/pinokio/run/plugin/antigravity-cli-auto/pinokio.js"),
|
|
46
|
+
""
|
|
47
|
+
)
|
|
48
|
+
assert.strictEqual(
|
|
49
|
+
PluginSources.systemPluginPathForLocalPath("/api/example/plugins/helper/pinokio.js"),
|
|
50
|
+
""
|
|
51
|
+
)
|
|
52
|
+
})
|
|
53
|
+
|
|
35
54
|
test("normalizeActionPathComponents maps action URLs back to filesystem roots", () => {
|
|
36
55
|
assert.deepStrictEqual(
|
|
37
56
|
PluginSources.normalizeActionPathComponents(["pinokio", "run", "plugin", "antigravity-cli", "pinokio.js"]),
|