pinokiod 7.3.14 → 7.4.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/kernel/api/index.js +111 -15
- package/kernel/api/script/index.js +10 -0
- package/kernel/autolaunch.js +111 -0
- package/kernel/environment.js +89 -1
- package/kernel/git.js +9 -19
- package/kernel/index.js +142 -43
- package/kernel/launch_requirements.js +1115 -0
- package/kernel/ready.js +231 -0
- package/kernel/script.js +16 -0
- package/kernel/shells.js +9 -1
- package/kernel/workspace_status.js +111 -45
- package/package.json +1 -1
- package/server/autolaunch.js +725 -0
- package/server/index.js +244 -411
- package/server/public/logs.js +15 -1
- package/server/public/style.css +99 -31
- package/server/views/app.ejs +263 -159
- package/server/views/autolaunch.ejs +363 -75
- package/server/views/index.ejs +550 -26
- package/server/views/logs.ejs +14 -12
- package/server/views/partials/app_autolaunch_dependency_events.ejs +99 -0
- package/server/views/partials/app_autolaunch_dependency_save.ejs +10 -0
- package/server/views/partials/app_autolaunch_dependency_styles.ejs +357 -0
- package/server/views/partials/app_autolaunch_modal_helpers.ejs +347 -0
- package/server/views/partials/autolaunch_dependency_helpers.ejs +204 -0
- package/server/views/partials/autolaunch_dependency_save.ejs +13 -0
- package/server/views/partials/autolaunch_dependency_styles.ejs +347 -0
- package/server/views/partials/home_action_modal.ejs +4 -1
- package/server/views/partials/launch_requirements_status_client.ejs +271 -0
- package/server/views/partials/launch_requirements_status_styles.ejs +171 -0
- package/server/views/partials/launch_settings_dependency_save_factory.ejs +45 -0
- package/server/views/partials/launch_settings_dependency_script_loader_factory.ejs +25 -0
- package/server/views/terminal.ejs +196 -2
- package/test/home-autolaunch-live-ui.test.js +455 -0
- package/test/launch-requirements-browser.test.js +579 -0
- package/test/launch-requirements-contract-coverage.test.js +627 -0
- package/test/launch-requirements-real-browser.js +625 -0
- package/test/launch-requirements-status-client.test.js +132 -0
- package/test/launch-requirements.test.js +1806 -0
- package/test/launch-settings-ui.test.js +370 -0
- package/test/ready-state.test.js +49 -0
- package/test/server-autolaunch.test.js +1052 -0
- package/test/startup-git-index-benchmark.js +409 -0
- package/test/startup-git-index-browser.js +320 -0
- package/test/startup-git-index-performance.test.js +380 -0
- package/test/startup-git-index-refactor.test.js +450 -0
- package/test/startup-git-index-route.test.js +588 -0
- package/test/universal-launcher.smoke.spec.js +10 -9
- package/test/workspace-gitignore-benchmark.js +815 -0
- package/test/workspace-gitignore-path-scoped.test.js +256 -0
- package/spec/INSTRUCTION_SYNC.md +0 -432
|
@@ -0,0 +1,370 @@
|
|
|
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
|
+
|
|
6
|
+
const root = path.resolve(__dirname, "..")
|
|
7
|
+
|
|
8
|
+
test("launch settings only checks a script when it is persisted", async () => {
|
|
9
|
+
const appView = await fs.readFile(path.resolve(root, "server/views/app.ejs"), "utf8")
|
|
10
|
+
const appHelpers = await fs.readFile(path.resolve(root, "server/views/partials/app_autolaunch_modal_helpers.ejs"), "utf8")
|
|
11
|
+
const globalView = await fs.readFile(path.resolve(root, "server/views/autolaunch.ejs"), "utf8")
|
|
12
|
+
|
|
13
|
+
assert.match(appHelpers, /const configuredScript =/)
|
|
14
|
+
assert.doesNotMatch(appHelpers, /const chooseFallbackScript =/)
|
|
15
|
+
assert.doesNotMatch(globalView, /const chooseFallbackScript =/)
|
|
16
|
+
assert.doesNotMatch(appHelpers, /if \(!selectedScript\)\s*\{\s*selectedScript = choose/)
|
|
17
|
+
assert.match(appView, /selectedScript = configuredScript\(data\)/)
|
|
18
|
+
assert.match(globalView, /selectedScript = configuredScript\(data\)/)
|
|
19
|
+
assert.doesNotMatch(appView, /selectedScript = chooseInitialScript\(data\)/)
|
|
20
|
+
assert.doesNotMatch(globalView, /selectedScript = chooseInitialScript\(data\)/)
|
|
21
|
+
assert.doesNotMatch(appView, /chooseFallbackScript/)
|
|
22
|
+
assert.doesNotMatch(globalView, /chooseFallbackScript/)
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
test("launch settings exposes explicit launch script clearing", async () => {
|
|
26
|
+
const appView = await fs.readFile(path.resolve(root, "server/views/app.ejs"), "utf8")
|
|
27
|
+
const appHelpers = await fs.readFile(path.resolve(root, "server/views/partials/app_autolaunch_modal_helpers.ejs"), "utf8")
|
|
28
|
+
const globalView = await fs.readFile(path.resolve(root, "server/views/autolaunch.ejs"), "utf8")
|
|
29
|
+
const route = await fs.readFile(path.resolve(root, "server/autolaunch.js"), "utf8")
|
|
30
|
+
|
|
31
|
+
assert.match(route, /clear_script/)
|
|
32
|
+
assert.match(appView, /data-app-autolaunch-clear-script/)
|
|
33
|
+
assert.match(appView, /body: JSON\.stringify\(\{ app: appId, clear_script: true \}\)/)
|
|
34
|
+
assert.match(appHelpers, /data-app-autolaunch-clear-script/)
|
|
35
|
+
assert.match(appHelpers, /const hasRequirements = dependencies\.length > 0/)
|
|
36
|
+
assert.match(appHelpers, /Remove requirements before clearing this app's launch script\./)
|
|
37
|
+
assert.match(appHelpers, /\$\{hasRequirements \? "disabled" : ""\}/)
|
|
38
|
+
assert.match(appHelpers, /Choose this app\\'s launch script before these requirements can run\./)
|
|
39
|
+
assert.match(appHelpers, /selectedScript = updated\.autolaunch \|\| ""/)
|
|
40
|
+
assert.match(globalView, /data-clear-script/)
|
|
41
|
+
assert.match(globalView, /const hasRequirements = dependencies\.length > 0/)
|
|
42
|
+
assert.match(globalView, /Remove requirements before clearing this app's launch script\./)
|
|
43
|
+
assert.match(globalView, /\$\{hasRequirements \? "disabled" : ""\}/)
|
|
44
|
+
assert.match(globalView, /Choose this app\\'s launch script before these requirements can run\./)
|
|
45
|
+
assert.match(globalView, /clear_script: true/)
|
|
46
|
+
assert.match(globalView, /selectedScript = ""/)
|
|
47
|
+
assert.match(route, /Remove requirements before clearing this app's launch script\./)
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
test("app page does not fabricate launch requirement status from saved config", async () => {
|
|
51
|
+
const appView = await fs.readFile(path.resolve(root, "server/views/app.ejs"), "utf8")
|
|
52
|
+
const server = await fs.readFile(path.resolve(root, "server/index.js"), "utf8")
|
|
53
|
+
const homeAutolaunch = await fs.readFile(path.resolve(root, "server/autolaunch.js"), "utf8")
|
|
54
|
+
|
|
55
|
+
assert.doesNotMatch(appView, /provisionalStatus/)
|
|
56
|
+
assert.doesNotMatch(appView, /initialRequirementRows/)
|
|
57
|
+
assert.doesNotMatch(appView, /autolaunch_dependency_apps/)
|
|
58
|
+
assert.doesNotMatch(server, /autolaunch_dependency_apps/)
|
|
59
|
+
assert.match(homeAutolaunch, /const configuredDependencies =/)
|
|
60
|
+
assert.match(homeAutolaunch, /const waitingFor = autolaunchStatus \? statusWaitingFor : configuredDependencies/)
|
|
61
|
+
assert.doesNotMatch(appView, /data\.(running|ready|script_state|running_scripts)/)
|
|
62
|
+
assert.doesNotMatch(server, /script_state/)
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
test("launch settings visible labels use autolaunch wording", async () => {
|
|
66
|
+
const appView = await fs.readFile(path.resolve(root, "server/views/app.ejs"), "utf8")
|
|
67
|
+
const globalView = await fs.readFile(path.resolve(root, "server/views/autolaunch.ejs"), "utf8")
|
|
68
|
+
const sidebar = await fs.readFile(path.resolve(root, "server/views/partials/main_sidebar.ejs"), "utf8")
|
|
69
|
+
|
|
70
|
+
assert.match(appView, />Autolaunch</)
|
|
71
|
+
assert.match(globalView, /<h1 class="task-title">Autolaunch<\/h1>/)
|
|
72
|
+
assert.match(sidebar, />Autolaunch</)
|
|
73
|
+
assert.doesNotMatch(globalView, /<h1 class="task-title">Launch settings<\/h1>/)
|
|
74
|
+
assert.doesNotMatch(sidebar, />Launch settings</)
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
test("startup progress uses canonical ready-state progress instead of autolaunch mirrors", async () => {
|
|
78
|
+
const kernelIndex = await fs.readFile(path.resolve(root, "kernel/index.js"), "utf8")
|
|
79
|
+
const autolaunch = await fs.readFile(path.resolve(root, "kernel/autolaunch.js"), "utf8")
|
|
80
|
+
const homeAutolaunch = await fs.readFile(path.resolve(root, "server/autolaunch.js"), "utf8")
|
|
81
|
+
|
|
82
|
+
assert.doesNotMatch(kernelIndex, /autolaunch\.updateProgress/)
|
|
83
|
+
assert.doesNotMatch(autolaunch, /updateProgress/)
|
|
84
|
+
assert.match(homeAutolaunch, /getScriptProgress\(launchPath\)/)
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
test("app page launch requirement status uses the shared target status model", async () => {
|
|
88
|
+
const kernelIndex = await fs.readFile(path.resolve(root, "kernel/index.js"), "utf8")
|
|
89
|
+
const launchRequirements = await fs.readFile(path.resolve(root, "kernel/launch_requirements.js"), "utf8")
|
|
90
|
+
const autolaunch = await fs.readFile(path.resolve(root, "kernel/autolaunch.js"), "utf8")
|
|
91
|
+
|
|
92
|
+
assert.match(kernelIndex, /return this\.launchRequirements\.getStatus\(appId\)/)
|
|
93
|
+
assert.match(kernelIndex, /launchRequirements\.markStartupStarted\(appId\)/)
|
|
94
|
+
assert.match(kernelIndex, /launchRequirements\.markStartupReady\(appId\)/)
|
|
95
|
+
assert.match(kernelIndex, /launchRequirements\.markStartupStopped\(appId\)/)
|
|
96
|
+
assert.doesNotMatch(kernelIndex, /launchRequirements\.getStartupStatus/)
|
|
97
|
+
assert.doesNotMatch(launchRequirements, /getStartupStatus\s*\(/)
|
|
98
|
+
assert.doesNotMatch(launchRequirements, /launchStartupRoots\s*\(/)
|
|
99
|
+
assert.match(launchRequirements, /markStartupStarted\(appId\)/)
|
|
100
|
+
assert.doesNotMatch(autolaunch, /launchStartupRoots/)
|
|
101
|
+
assert.doesNotMatch(autolaunch, /this\.status/)
|
|
102
|
+
assert.doesNotMatch(autolaunch, /markStarted\(appId\)/)
|
|
103
|
+
assert.doesNotMatch(autolaunch, /markReady\(appId\)/)
|
|
104
|
+
assert.doesNotMatch(autolaunch, /markStopped\(appId\)/)
|
|
105
|
+
assert.match(autolaunch, /startup: true/)
|
|
106
|
+
})
|
|
107
|
+
|
|
108
|
+
test("app page launch requirement status uses socket events instead of polling forever", async () => {
|
|
109
|
+
const appView = await fs.readFile(path.resolve(root, "server/views/app.ejs"), "utf8")
|
|
110
|
+
const statusClient = await fs.readFile(path.resolve(root, "server/views/partials/launch_requirements_status_client.ejs"), "utf8")
|
|
111
|
+
const launchRequirements = await fs.readFile(path.resolve(root, "kernel/launch_requirements.js"), "utf8")
|
|
112
|
+
|
|
113
|
+
assert.match(appView, /partials\/launch_requirements_status_client/)
|
|
114
|
+
assert.match(statusClient, /new Socket\(\)/)
|
|
115
|
+
assert.match(statusClient, /method: channel/)
|
|
116
|
+
assert.match(statusClient, /type !== "launch\.requirements"/)
|
|
117
|
+
assert.doesNotMatch(statusClient, /setTimeout\(poll,\s*1000\)/)
|
|
118
|
+
assert.match(launchRequirements, /kernel\.launch_requirements:\$\{id\}/)
|
|
119
|
+
assert.match(launchRequirements, /type: "launch\.requirements"/)
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
test("app page launch requirement status client is inert until launch config exists", async () => {
|
|
123
|
+
const server = await fs.readFile(path.resolve(root, "server/index.js"), "utf8")
|
|
124
|
+
const appView = await fs.readFile(path.resolve(root, "server/views/app.ejs"), "utf8")
|
|
125
|
+
const statusClient = await fs.readFile(path.resolve(root, "server/views/partials/launch_requirements_status_client.ejs"), "utf8")
|
|
126
|
+
|
|
127
|
+
assert.match(server, /launch_requirements_status_enabled/)
|
|
128
|
+
assert.match(server, /autolaunchAppState\.autolaunch/)
|
|
129
|
+
assert.match(server, /autolaunchAppState\.autolaunch_depends/)
|
|
130
|
+
assert.match(statusClient, /const initialEnabled =/)
|
|
131
|
+
assert.match(statusClient, /if \(initialEnabled\) \{[\s\S]*startStatusClient\(\)[\s\S]*\} else \{[\s\S]*pinokio:launch-requirements-configured/)
|
|
132
|
+
const startIndex = statusClient.indexOf("const startStatusClient = () => {")
|
|
133
|
+
const gatedFetchIndex = statusClient.indexOf("fetchInitialStatus()", startIndex)
|
|
134
|
+
const gateIndex = statusClient.indexOf("if (initialEnabled)")
|
|
135
|
+
assert.ok(startIndex >= 0)
|
|
136
|
+
assert.ok(gatedFetchIndex > startIndex)
|
|
137
|
+
assert.ok(gatedFetchIndex < gateIndex)
|
|
138
|
+
assert.match(appView, /const hasLaunchRequirementConfig = \(\) =>/)
|
|
139
|
+
assert.match(appView, /window\.dispatchEvent\(new CustomEvent\("pinokio:launch-requirements-configured"\)\)/)
|
|
140
|
+
assert.match(appView, /notifyLaunchRequirementsConfigured\(\)/)
|
|
141
|
+
})
|
|
142
|
+
|
|
143
|
+
test("embedded terminal launch requirement status defers to parent app overlay", async () => {
|
|
144
|
+
const statusClient = await fs.readFile(path.resolve(root, "server/views/partials/launch_requirements_status_client.ejs"), "utf8")
|
|
145
|
+
|
|
146
|
+
assert.match(statusClient, /shouldDeferToParentLaunchRequirements/)
|
|
147
|
+
assert.match(statusClient, /window\.self === window\.top/)
|
|
148
|
+
assert.match(statusClient, /window\.parent && window\.parent\.document/)
|
|
149
|
+
assert.match(statusClient, /parentDocument\.querySelector\("\[data-launch-requirements-status\]"\)/)
|
|
150
|
+
assert.match(statusClient, /if \(shouldDeferToParentLaunchRequirements\(\)\) return/)
|
|
151
|
+
})
|
|
152
|
+
|
|
153
|
+
test("app page launch requirement status shows spinners on active rows", async () => {
|
|
154
|
+
const statusClient = await fs.readFile(path.resolve(root, "server/views/partials/launch_requirements_status_client.ejs"), "utf8")
|
|
155
|
+
const styles = await fs.readFile(path.resolve(root, "server/views/partials/launch_requirements_status_styles.ejs"), "utf8")
|
|
156
|
+
|
|
157
|
+
assert.match(statusClient, /const isActiveRow =/)
|
|
158
|
+
assert.match(statusClient, /launch-requirements-state\$\{stateClass\(row\)\}/)
|
|
159
|
+
assert.match(statusClient, /fa-circle-notch fa-spin/)
|
|
160
|
+
assert.doesNotMatch(statusClient, /launch-requirements-title">\s*<i class="fa-solid fa-circle-notch fa-spin"/)
|
|
161
|
+
assert.match(styles, /\.launch-requirements-state\.is-active i/)
|
|
162
|
+
})
|
|
163
|
+
|
|
164
|
+
test("app page launch requirement status explains waiting rows and marks ready rows", async () => {
|
|
165
|
+
const statusClient = await fs.readFile(path.resolve(root, "server/views/partials/launch_requirements_status_client.ejs"), "utf8")
|
|
166
|
+
|
|
167
|
+
assert.match(statusClient, /const waitingForLabel =/)
|
|
168
|
+
assert.match(statusClient, /Waiting for \$\{names\.join\(", "\)\}/)
|
|
169
|
+
assert.match(statusClient, /stateLabel\(row, titleById\)/)
|
|
170
|
+
assert.match(statusClient, /const isReadyRow =/)
|
|
171
|
+
assert.match(statusClient, /fa-solid fa-check/)
|
|
172
|
+
assert.match(statusClient, /is-ready/)
|
|
173
|
+
})
|
|
174
|
+
|
|
175
|
+
test("static guard: home autolaunch live status discovers rows after initial render", async () => {
|
|
176
|
+
const homeView = await fs.readFile(path.resolve(root, "server/views/index.ejs"), "utf8")
|
|
177
|
+
|
|
178
|
+
assert.match(homeView, /const getAutolaunchLines = \(state\) =>/)
|
|
179
|
+
assert.match(homeView, /const requestHomeStatus = \(\) =>/)
|
|
180
|
+
assert.match(homeView, /new XMLHttpRequest\(\)/)
|
|
181
|
+
assert.doesNotMatch(homeView, /fetch\("\/pinokio\/home_status"/)
|
|
182
|
+
assert.ok(homeView.includes('querySelectorAll(".home-app-line[data-autolaunch-app]")'))
|
|
183
|
+
assert.match(homeView, /const runningApps = new Set\(Array\.isArray\(state && state\.running_apps\)/)
|
|
184
|
+
assert.match(homeView, /const trackedApps = new Set\(Object\.entries\(apps\)/)
|
|
185
|
+
assert.match(homeView, /const shouldDisplayAutolaunchStatus = \(app, status, apps, runningApps\) =>/)
|
|
186
|
+
assert.match(homeView, /shouldDisplayAutolaunchStatus\(app, status, apps, runningApps\)/)
|
|
187
|
+
assert.match(homeView, /for \(const app of runningApps\) \{[\s\S]*trackedApps\.add\(app\)/)
|
|
188
|
+
assert.match(homeView, /autolaunchState && !shouldDisplayAutolaunchStatus\(app, autolaunchState, apps, runningApps\)/)
|
|
189
|
+
assert.doesNotMatch(homeView, /<% if \(!launch_complete\) \{ %>\s*const startHomeAutolaunchPolling/)
|
|
190
|
+
assert.match(homeView, /const initialLaunchComplete = <%= launch_complete \? "true" : "false" %>/)
|
|
191
|
+
assert.match(homeView, /if \(initialLaunchComplete && !hasVisibleStartupRows\(\)\) \{\s*return\s*\}/)
|
|
192
|
+
assert.match(homeView, /const maxPollAttempts = 240/)
|
|
193
|
+
assert.match(homeView, /attempts \+= 1/)
|
|
194
|
+
assert.match(homeView, /const shouldContinueHomeStatusPolling = \(state\) =>/)
|
|
195
|
+
assert.match(homeView, /if \(!shouldContinueHomeStatusPolling\(state\)\) \{\s*stopPolling\(\)\s*\}/)
|
|
196
|
+
assert.match(homeView, /if \(hasVisibleStartupRows\(\)\) \{\s*return true\s*\}/)
|
|
197
|
+
assert.match(homeView, /return !\(state && state\.launch_complete\)/)
|
|
198
|
+
assert.match(homeView, /if \(isPreparingAutolaunchState\(autolaunchState\)\)/)
|
|
199
|
+
assert.doesNotMatch(homeView, /autolaunchState && !\["ready", "blocked", "failed", "timeout"\]\.includes\(autolaunchState\.state \|\| ""\) && !state\.launch_complete/)
|
|
200
|
+
assert.match(homeView, /chip = document\.createElement\("span"\)/)
|
|
201
|
+
assert.match(homeView, /chip\.className = "home-autolaunch-status"/)
|
|
202
|
+
assert.match(homeView, /notRunning\.forEach[\s\S]*data-autolaunch-app="<%=item\.uri%>"/)
|
|
203
|
+
assert.match(homeView, /console\.warn\("\[home\] autolaunch status update failed", error\)/)
|
|
204
|
+
assert.doesNotMatch(homeView, /catch \(error\) \{\s*\}/)
|
|
205
|
+
})
|
|
206
|
+
|
|
207
|
+
test("home running buttons only show spinner while startup label is active", async () => {
|
|
208
|
+
const homeView = await fs.readFile(path.resolve(root, "server/views/index.ejs"), "utf8")
|
|
209
|
+
|
|
210
|
+
assert.match(homeView, /const syncStopButtonSpinner = \(button, show\) =>/)
|
|
211
|
+
assert.match(homeView, /syncStopButtonSpinner\(button, !!displayLabel\)/)
|
|
212
|
+
assert.match(homeView, /<% if \(isStartingAutolaunchScript\) \{ %>\s*<i class='home-stop-spinner fa-solid fa-spin fa-circle-notch'/)
|
|
213
|
+
assert.match(homeView, /const active = new Set\(\)/)
|
|
214
|
+
assert.match(homeView, /if \(!active\.has\(identity\)\) \{\s*button\.remove\(\)/)
|
|
215
|
+
assert.match(homeView, /const markAutolaunchIdle = \(line\) => \{[\s\S]*syncStopButtons\(line, \[\]\)/)
|
|
216
|
+
})
|
|
217
|
+
|
|
218
|
+
test("launch requirements are not controlled by DOM markers or query parameters", async () => {
|
|
219
|
+
const appView = await fs.readFile(path.resolve(root, "server/views/app.ejs"), "utf8")
|
|
220
|
+
const terminalView = await fs.readFile(path.resolve(root, "server/views/terminal.ejs"), "utf8")
|
|
221
|
+
const menuPartial = await fs.readFile(path.resolve(root, "server/views/partials/menu.ejs"), "utf8")
|
|
222
|
+
const dynamicPartial = await fs.readFile(path.resolve(root, "server/views/partials/dynamic.ejs"), "utf8")
|
|
223
|
+
const launchRequirements = await fs.readFile(path.resolve(root, "kernel/launch_requirements.js"), "utf8")
|
|
224
|
+
|
|
225
|
+
assert.doesNotMatch(menuPartial, /data-launch-requirements-primary/)
|
|
226
|
+
assert.doesNotMatch(dynamicPartial, /data-launch-requirements-primary/)
|
|
227
|
+
assert.doesNotMatch(appView, /__pinokio_launch_requirements/)
|
|
228
|
+
assert.doesNotMatch(terminalView, /launchRequirementsPrimary/)
|
|
229
|
+
assert.doesNotMatch(terminalView, /__pinokio_launch_requirements/)
|
|
230
|
+
assert.doesNotMatch(terminalView, /launch_requirements_primary/)
|
|
231
|
+
assert.doesNotMatch(launchRequirements, /launch_requirements_primary/)
|
|
232
|
+
assert.match(launchRequirements, /configuredLaunchScript && requestedScript === config\.configuredLaunchScript/)
|
|
233
|
+
})
|
|
234
|
+
|
|
235
|
+
test("launch settings dependency save flow is shared", async () => {
|
|
236
|
+
const factory = await fs.readFile(path.resolve(root, "server/views/partials/launch_settings_dependency_save_factory.ejs"), "utf8")
|
|
237
|
+
const appSave = await fs.readFile(path.resolve(root, "server/views/partials/app_autolaunch_dependency_save.ejs"), "utf8")
|
|
238
|
+
const pageSave = await fs.readFile(path.resolve(root, "server/views/partials/autolaunch_dependency_save.ejs"), "utf8")
|
|
239
|
+
|
|
240
|
+
assert.match(factory, /fetch\("\/autolaunch\/dependencies"/)
|
|
241
|
+
assert.match(appSave, /createLaunchSettingsDependencySaver/)
|
|
242
|
+
assert.match(pageSave, /createLaunchSettingsDependencySaver/)
|
|
243
|
+
assert.doesNotMatch(appSave, /fetch\("\/autolaunch\/dependencies"/)
|
|
244
|
+
assert.doesNotMatch(pageSave, /fetch\("\/autolaunch\/dependencies"/)
|
|
245
|
+
})
|
|
246
|
+
|
|
247
|
+
test("launch settings block adding requirements without owning launch script", async () => {
|
|
248
|
+
const appEvents = await fs.readFile(path.resolve(root, "server/views/partials/app_autolaunch_dependency_events.ejs"), "utf8")
|
|
249
|
+
const pageView = await fs.readFile(path.resolve(root, "server/views/autolaunch.ejs"), "utf8")
|
|
250
|
+
|
|
251
|
+
assert.match(appEvents, /selectedScript \|\| state\.autolaunch/)
|
|
252
|
+
assert.match(appEvents, /Choose this app's launch script before adding requirements\./)
|
|
253
|
+
assert.match(pageView, /selectedScript \|\| app\.autolaunch/)
|
|
254
|
+
assert.match(pageView, /Choose this app's launch script before adding requirements\./)
|
|
255
|
+
})
|
|
256
|
+
|
|
257
|
+
test("launch settings UI sends selected script with explicit startup enabled state", async () => {
|
|
258
|
+
const appView = await fs.readFile(path.resolve(root, "server/views/app.ejs"), "utf8")
|
|
259
|
+
const globalView = await fs.readFile(path.resolve(root, "server/views/autolaunch.ejs"), "utf8")
|
|
260
|
+
|
|
261
|
+
assert.match(appView, /saveScript\(selectedScript, !!state\.autolaunch_enabled\)/)
|
|
262
|
+
assert.match(globalView, /saveScript\(selectedScript, !!app\.autolaunch_enabled\)/)
|
|
263
|
+
})
|
|
264
|
+
|
|
265
|
+
test("launch settings dependency script loader is shared", async () => {
|
|
266
|
+
const factory = await fs.readFile(path.resolve(root, "server/views/partials/launch_settings_dependency_script_loader_factory.ejs"), "utf8")
|
|
267
|
+
const appView = await fs.readFile(path.resolve(root, "server/views/app.ejs"), "utf8")
|
|
268
|
+
const pageView = await fs.readFile(path.resolve(root, "server/views/autolaunch.ejs"), "utf8")
|
|
269
|
+
|
|
270
|
+
assert.match(factory, /fetch\(`\/autolaunch\/candidates\?app=\$\{encodeURIComponent\(appId\)\}`/)
|
|
271
|
+
assert.match(appView, /createLaunchSettingsDependencyScriptLoader/)
|
|
272
|
+
assert.match(pageView, /createLaunchSettingsDependencyScriptLoader/)
|
|
273
|
+
assert.doesNotMatch(appView, /dependencyScriptCandidates\[dependencyId\] = \{ loading: true \}/)
|
|
274
|
+
assert.doesNotMatch(pageView, /dependencyScriptCandidates\[appId\] = \{ loading: true \}/)
|
|
275
|
+
})
|
|
276
|
+
|
|
277
|
+
test("launch settings opens dependency script picker before saving a new dependency", async () => {
|
|
278
|
+
const appView = await fs.readFile(path.resolve(root, "server/views/app.ejs"), "utf8")
|
|
279
|
+
const appEvents = await fs.readFile(path.resolve(root, "server/views/partials/app_autolaunch_dependency_events.ejs"), "utf8")
|
|
280
|
+
const pageView = await fs.readFile(path.resolve(root, "server/views/autolaunch.ejs"), "utf8")
|
|
281
|
+
const appHelpers = await fs.readFile(path.resolve(root, "server/views/partials/app_autolaunch_modal_helpers.ejs"), "utf8")
|
|
282
|
+
const pageHelpers = await fs.readFile(path.resolve(root, "server/views/partials/autolaunch_dependency_helpers.ejs"), "utf8")
|
|
283
|
+
const appStyles = await fs.readFile(path.resolve(root, "server/views/partials/app_autolaunch_dependency_styles.ejs"), "utf8")
|
|
284
|
+
const pageStyles = await fs.readFile(path.resolve(root, "server/views/partials/autolaunch_dependency_styles.ejs"), "utf8")
|
|
285
|
+
|
|
286
|
+
assert.match(appEvents, /pendingDependencyId = dependency[\s\S]*dependencyScriptPickerOpen = dependency[\s\S]*loadDependencyScriptCandidates\(dependency\)/)
|
|
287
|
+
assert.match(pageView, /pendingDependencyId = dependency;[\s\S]*dependencyScriptPickerOpen = dependency;[\s\S]*loadDependencyScriptCandidates\(dependency\);/)
|
|
288
|
+
assert.match(appView, /if \(pendingDependencyId === dependencyId\) \{[\s\S]*dependencies\.concat\(dependencyId\)[\s\S]*"Requirement saved\."/)
|
|
289
|
+
assert.match(pageView, /if \(pendingDependencyId === appId\) \{[\s\S]*dependencies\.concat\(appId\)[\s\S]*"Requirement saved\."/)
|
|
290
|
+
assert.match(appHelpers, /app-autolaunch-dependency-script-modal/)
|
|
291
|
+
assert.match(appHelpers, /data-app-autolaunch-dependency-script-choice/)
|
|
292
|
+
assert.match(appHelpers, /data-app-autolaunch-confirm-dependency-script/)
|
|
293
|
+
assert.match(appHelpers, /app-autolaunch-primary-script/)
|
|
294
|
+
assert.match(appStyles, /\.app-autolaunch-dependency-script-actions \{[\s\S]*position: sticky;/)
|
|
295
|
+
assert.match(appStyles, /\.app-autolaunch-primary \{[\s\S]*appearance: none;[\s\S]*width: 100%;/)
|
|
296
|
+
assert.match(pageHelpers, /autolaunch-dependency-script-modal/)
|
|
297
|
+
assert.match(pageHelpers, /data-dependency-script-choice/)
|
|
298
|
+
assert.match(pageHelpers, /data-confirm-dependency-script/)
|
|
299
|
+
assert.match(pageHelpers, /autolaunch-primary-script/)
|
|
300
|
+
assert.match(pageStyles, /\.autolaunch-dependency-script-actions \{[\s\S]*position: sticky;/)
|
|
301
|
+
assert.match(pageStyles, /\.autolaunch-primary \{[\s\S]*appearance: none;[\s\S]*width: 100%;/)
|
|
302
|
+
assert.doesNotMatch(appHelpers, /Choose launch script/)
|
|
303
|
+
assert.doesNotMatch(pageHelpers, /Choose launch script/)
|
|
304
|
+
})
|
|
305
|
+
|
|
306
|
+
test("app launch settings busy state preserves rule-disabled controls", async () => {
|
|
307
|
+
const appHelpers = await fs.readFile(path.resolve(root, "server/views/partials/app_autolaunch_modal_helpers.ejs"), "utf8")
|
|
308
|
+
|
|
309
|
+
assert.match(appHelpers, /data-app-autolaunch-disabled-before-busy/)
|
|
310
|
+
assert.match(appHelpers, /control\.setAttribute\(attr, control\.disabled \? "true" : "false"\)/)
|
|
311
|
+
assert.match(appHelpers, /control\.disabled = previous === "true"/)
|
|
312
|
+
assert.doesNotMatch(appHelpers, /scriptsEl\.querySelectorAll\("button"\)[\s\S]*control\.disabled = saving \|\| savingDependencyScript/)
|
|
313
|
+
assert.doesNotMatch(appHelpers, /dependenciesEl\.querySelectorAll\("button, input"\)[\s\S]*control\.disabled = saving \|\| savingDependencies \|\| savingDependencyScript/)
|
|
314
|
+
})
|
|
315
|
+
|
|
316
|
+
test("static guard: blocked requirement choose script has an app-page handler and terminal fallback", async () => {
|
|
317
|
+
const appView = await fs.readFile(path.resolve(root, "server/views/app.ejs"), "utf8")
|
|
318
|
+
const statusClient = await fs.readFile(path.resolve(root, "server/views/partials/launch_requirements_status_client.ejs"), "utf8")
|
|
319
|
+
const styles = await fs.readFile(path.resolve(root, "server/views/partials/launch_requirements_status_styles.ejs"), "utf8")
|
|
320
|
+
|
|
321
|
+
assert.match(statusClient, /new CustomEvent\("pinokio:launch-requirements-choose-script", \{[\s\S]*cancelable: true/)
|
|
322
|
+
assert.match(statusClient, /class="fa-solid fa-file-code"/)
|
|
323
|
+
assert.match(styles, /\.launch-requirements-choose-script \{[\s\S]*appearance: none;/)
|
|
324
|
+
assert.match(styles, /\.launch-requirements-choose-script \{[\s\S]*font: inherit;/)
|
|
325
|
+
assert.match(styles, /\.launch-requirements-choose-script \{[\s\S]*border-radius: 6px;/)
|
|
326
|
+
assert.match(styles, /\.launch-requirements-choose-script:focus-visible/)
|
|
327
|
+
assert.match(statusClient, /if \(!chooseEvent\.defaultPrevented\) \{[\s\S]*window\.location\.href = `\/v\/\$\{encodeURIComponent\(dependencyApp\)\}\?autolaunch=1`/)
|
|
328
|
+
assert.match(appView, /pinokio:launch-requirements-choose-script[\s\S]*event\.preventDefault\(\)/)
|
|
329
|
+
assert.match(appView, /params\.get\("autolaunch"\) !== "1"/)
|
|
330
|
+
assert.match(appView, /setOpen\(true\)[\s\S]*await loadCandidates\(\)/)
|
|
331
|
+
})
|
|
332
|
+
|
|
333
|
+
test("static guard: terminal resets run control on launch requirement control packets", async () => {
|
|
334
|
+
const terminalView = await fs.readFile(path.resolve(root, "server/views/terminal.ejs"), "utf8")
|
|
335
|
+
const statusClient = await fs.readFile(path.resolve(root, "server/views/partials/launch_requirements_status_client.ejs"), "utf8")
|
|
336
|
+
|
|
337
|
+
assert.match(terminalView, /packet\.type === "launch\.requirements\.control"/)
|
|
338
|
+
assert.match(terminalView, /packet\.type === "launch\.requirements\.control"[\s\S]*runControls\.set\("idle"\)/)
|
|
339
|
+
assert.match(terminalView, /packet\.type === "launch\.requirements\.control"[\s\S]*#progress-window[\s\S]*classList\.add\("hidden"\)/)
|
|
340
|
+
assert.match(statusClient, /notifyLaunchRequirementsControl\({[\s\S]*action: "cancelled"[\s\S]*app_id: appId/)
|
|
341
|
+
assert.match(statusClient, /window\.frames\[i\]\.postMessage\(payload, "\*"\)/)
|
|
342
|
+
assert.match(terminalView, /window\.addEventListener\("message"[\s\S]*data\.type !== "pinokio:launch-requirements-control"[\s\S]*runControls\.set\("idle"\)/)
|
|
343
|
+
})
|
|
344
|
+
|
|
345
|
+
test("static guard: open without launching is not wired to launch requirement status", async () => {
|
|
346
|
+
const homeView = await fs.readFile(path.resolve(root, "server/views/index.ejs"), "utf8")
|
|
347
|
+
const homeActionModal = await fs.readFile(path.resolve(root, "server/views/partials/home_action_modal.ejs"), "utf8")
|
|
348
|
+
const statusClient = await fs.readFile(path.resolve(root, "server/views/partials/launch_requirements_status_client.ejs"), "utf8")
|
|
349
|
+
|
|
350
|
+
assert.match(homeActionModal, /Open without launching/)
|
|
351
|
+
assert.match(homeActionModal, /class='home-mode-command home-browse' data-src='<%= item\.view_url %>'/)
|
|
352
|
+
assert.doesNotMatch(homeActionModal, /launch.requirements|data-launch-requirements|frame-link/)
|
|
353
|
+
assert.match(homeView, /if \(target\) \{[\s\S]*window\.PinokioHomeGuardNavigate\(src\)[\s\S]*location\.href = src/)
|
|
354
|
+
assert.match(statusClient, /const status = data && data\.ok \? data\.status : null/)
|
|
355
|
+
assert.doesNotMatch(statusClient, /saved requirement/)
|
|
356
|
+
})
|
|
357
|
+
|
|
358
|
+
test("static guard: open without launching disables page-load script frame selection", async () => {
|
|
359
|
+
const appView = await fs.readFile(path.resolve(root, "server/views/app.ejs"), "utf8")
|
|
360
|
+
|
|
361
|
+
assert.match(appView, /const openWithoutLaunching = <%- JSON\.stringify\(type === "run" && !autoselect\) %>/)
|
|
362
|
+
assert.match(appView, /const automaticSelectionDisabled = openWithoutLaunching && !triggeredByUser/)
|
|
363
|
+
assert.match(appView, /let persistedSelectionRaw = automaticSelectionDisabled \? null :/)
|
|
364
|
+
assert.match(appView, /const skipPersistedSelection = automaticSelectionDisabled \|\|/)
|
|
365
|
+
assert.match(appView, /if \(!target && !automaticSelectionDisabled && global_selector\)/)
|
|
366
|
+
assert.match(appView, /if \(!target && !automaticSelectionDisabled && !skipPersistedSelection\) \{[\s\S]*preselected =/)
|
|
367
|
+
assert.match(appView, /if \(!target && !automaticSelectionDisabled && persistedSelectionPayload\)/)
|
|
368
|
+
assert.match(appView, /if \(!target && !automaticSelectionDisabled && followCurrentDefault\)/)
|
|
369
|
+
assert.match(appView, /if \(!target && !automaticSelectionDisabled && preselected && preselected !== devTab\)/)
|
|
370
|
+
})
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
const assert = require("node:assert/strict")
|
|
2
|
+
const path = require("node:path")
|
|
3
|
+
const test = require("node:test")
|
|
4
|
+
|
|
5
|
+
const ReadyState = require("../kernel/ready")
|
|
6
|
+
|
|
7
|
+
test("ready state clears stale script progress on lifecycle transitions", () => {
|
|
8
|
+
const apiRoot = path.resolve("/tmp/pinokio-ready-state/api")
|
|
9
|
+
const launchPath = path.resolve(apiRoot, "app-a", "start.js")
|
|
10
|
+
const kernel = {
|
|
11
|
+
memory: {
|
|
12
|
+
rpc: {
|
|
13
|
+
[launchPath]: {
|
|
14
|
+
current: 1,
|
|
15
|
+
total: 2
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
},
|
|
19
|
+
path: (name) => name === "api" ? apiRoot : path.resolve("/tmp/pinokio-ready-state", name)
|
|
20
|
+
}
|
|
21
|
+
const ready = new ReadyState(kernel)
|
|
22
|
+
|
|
23
|
+
assert.deepEqual(ready.getScriptProgress(launchPath), {
|
|
24
|
+
step_current: 2,
|
|
25
|
+
step_total: 2
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
ready.markStarted(launchPath)
|
|
29
|
+
assert.equal(ready.getScriptProgress(launchPath), null)
|
|
30
|
+
assert.equal(ready.status.apps["app-a"].step_current, undefined)
|
|
31
|
+
assert.equal(ready.status.apps["app-a"].scripts["start.js"].step_current, undefined)
|
|
32
|
+
|
|
33
|
+
ready.markProgress(launchPath, 0, 2)
|
|
34
|
+
assert.deepEqual(ready.getScriptProgress(launchPath), {
|
|
35
|
+
step_current: 1,
|
|
36
|
+
step_total: 2
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
ready.markReady(launchPath)
|
|
40
|
+
assert.equal(ready.getScriptProgress(launchPath), null)
|
|
41
|
+
assert.equal(ready.status.apps["app-a"].step_current, undefined)
|
|
42
|
+
assert.equal(ready.status.apps["app-a"].scripts["start.js"].step_current, undefined)
|
|
43
|
+
|
|
44
|
+
ready.markProgress(launchPath, 1, 2)
|
|
45
|
+
ready.markStopped(launchPath)
|
|
46
|
+
assert.equal(ready.getScriptProgress(launchPath), null)
|
|
47
|
+
assert.equal(ready.status.apps["app-a"].step_current, undefined)
|
|
48
|
+
assert.equal(ready.status.apps["app-a"].scripts["start.js"].step_current, undefined)
|
|
49
|
+
})
|