The explainer panel used var(--surface, #fff) and PCDetail's Run Scripts and Corporate Tools styles used --text-primary, --text-secondary and --bg-secondary. None of those four tokens exist in this app's theme, so var() fell through: the explainer rendered a solid white block over the dark background and the surrounding copy lost its intended contrast. The real tokens are --bg-card, --text and --text-light. A fallback colour is what made this hard to catch - it looks correct in light mode and only misrenders on the theme the shop floor actually runs, so it would not have shown up in authoring or in review. Both files now carry a test asserting their styles reference only defined tokens and contain no var(--x, #colour) fallbacks. That test immediately caught a further dead --warning fallback in the banner. The two long per-field explanations in the Add Script form are now collapsed behind "How this works". The modal already scrolled, and three paragraphs of standing prose between the inputs pushed Save off screen, so the copy was being skimmed past rather than read.
96 lines
4.5 KiB
JavaScript
96 lines
4.5 KiB
JavaScript
/**
|
|
* Run Scripts tab on PC Details.
|
|
*
|
|
* The thing worth pinning here is the SEPARATION. run_script and
|
|
* scheduledtask_run map to different grants in plugins/endpoints/commands.py
|
|
* (endpoints.runscript and endpoints.corptools), split out of a single
|
|
* endpoints.deploy bundle precisely so that being able to trigger a corporate
|
|
* tool does not also mean being able to run arbitrary PowerShell as SYSTEM.
|
|
*
|
|
* That split lives in the backend. The UI can quietly undo it by hanging the
|
|
* script picker off the Corporate Tools tab, whose only gate is
|
|
* endpoints.corptools - and the failure would be invisible in review, because
|
|
* the page would look correct and the server would simply 403 the push. These
|
|
* tests fail if someone merges the two.
|
|
*
|
|
* Asserted against the file text, matching systemScriptsSettings.spec.js.
|
|
*/
|
|
import { describe, it, expect } from 'vitest'
|
|
import { readFileSync } from 'fs'
|
|
import { join, dirname } from 'path'
|
|
import { fileURLToPath } from 'url'
|
|
|
|
const here = dirname(fileURLToPath(import.meta.url))
|
|
const source = readFileSync(join(here, 'PCDetail.vue'), 'utf8')
|
|
|
|
describe('PCDetail Run Scripts tab', () => {
|
|
it('gates the tab on endpoints.runscript, not the Corporate Tools grant', () => {
|
|
expect(source).toMatch(
|
|
/const canRunScript = computed\(\(\) => authStore\.hasPermission\('endpoints\.runscript'\)\)/)
|
|
})
|
|
|
|
it('pushes run_script from its own tab, not from the Corporate Tools block', () => {
|
|
// The corptools block must not gain a run_script call: that would put the
|
|
// action behind endpoints.corptools in the UI while the server still
|
|
// demands endpoints.runscript.
|
|
const corptools = source.slice(
|
|
source.indexOf("activegroup === 'corptools'"),
|
|
source.indexOf("activegroup === 'runscripts'"))
|
|
expect(corptools).not.toMatch(/run_script/)
|
|
expect(source).toMatch(/activegroup === 'runscripts'/)
|
|
})
|
|
|
|
it('enqueues run_script with a script-id target', () => {
|
|
expect(source).toMatch(/'run_script', `script:\$\{script\.scriptid\}`/)
|
|
})
|
|
|
|
it('requires a typed confirmation before running', () => {
|
|
// Every script on this tab is arbitrary code, so unlike Corporate Tools
|
|
// there is no safe branch that gets a plain OK button.
|
|
const handler = source.slice(source.indexOf('async function confirmRunScript'))
|
|
const body = handler.slice(0, handler.indexOf('\n}\n'))
|
|
expect(body).toMatch(/Type RUN to confirm/)
|
|
expect(body).toMatch(/typed !== 'RUN'/)
|
|
})
|
|
|
|
it('warns that a script runs as SYSTEM before it is sent', () => {
|
|
const tab = source.slice(source.indexOf("activegroup === 'runscripts'"))
|
|
expect(tab.slice(0, 2000)).toMatch(/runs as SYSTEM/)
|
|
})
|
|
|
|
it('shows the run context and working directory read-only', () => {
|
|
// Both are authoring decisions made in the library. Offering an override
|
|
// here would mean the script that ran is not the script that was reviewed,
|
|
// so they must be rendered as facts and never bound with v-model.
|
|
const tab = source.slice(source.indexOf("activegroup === 'runscripts'"))
|
|
const block = tab.slice(0, tab.indexOf('runscripts-actions'))
|
|
expect(block).toMatch(/selectedscript\.workingdirectory/)
|
|
expect(block).toMatch(/selectedscript\.runcontext/)
|
|
expect(block).not.toMatch(/v-model="[^"]*workingdirectory/)
|
|
expect(block).not.toMatch(/v-model="[^"]*runcontext/)
|
|
})
|
|
|
|
it('offers only scripts that have a body', () => {
|
|
// A bodyless script is rejected at enqueue by _resolve_run_script, so
|
|
// listing it would offer a button whose only outcome is an error.
|
|
expect(source).toMatch(/filter\(\(script\) => script\.hasbody\)/)
|
|
})
|
|
|
|
it('reports the push as queued rather than as finished', () => {
|
|
// The agent collects the command on its next check-in; claiming it ran
|
|
// would assert an outcome nothing has observed.
|
|
const handler = source.slice(source.indexOf('async function confirmRunScript'))
|
|
expect(handler.slice(0, handler.indexOf('\n}\n'))).toMatch(/queued for/)
|
|
})
|
|
|
|
it('styles the Run Scripts tab from defined theme tokens only', () => {
|
|
// --text-primary/--text-secondary/--bg-secondary are not defined by this
|
|
// app's theme. var() then falls through to the fallback, or to inherit,
|
|
// which reads fine in light mode and misrenders for dark-mode users - the
|
|
// failure only shows up on the theme most of the shop floor runs.
|
|
const runscripts = source.slice(source.indexOf('.corptools-intro'))
|
|
expect(runscripts).not.toMatch(/--text-primary|--text-secondary|--bg-secondary|--surface/)
|
|
expect(runscripts).not.toMatch(/var\(--[a-z-]+,\s*#/)
|
|
})
|
|
})
|