visual fixes
This commit is contained in:
parent
e1239f3d93
commit
d7c22ca452
|
|
@ -157,6 +157,11 @@ toggles a `topbar--<state>` class):
|
||||||
(warning, not failure);
|
(warning, not failure);
|
||||||
- **done** — a static green line; **idle** — plain header.
|
- **done** — a static green line; **idle** — plain header.
|
||||||
|
|
||||||
|
Every duration is an integer multiple of a 1.4s beat, so where a state pairs two
|
||||||
|
animations (e.g. the running sweep + sheen) they repeat on a common period and
|
||||||
|
stay phase-locked instead of slowly drifting apart. Keep new timings on that beat
|
||||||
|
grid.
|
||||||
|
|
||||||
All movement is disabled under `prefers-reduced-motion: reduce` (the state colour
|
All movement is disabled under `prefers-reduced-motion: reduce` (the state colour
|
||||||
stays). See `public/styles.css` (`topbar--*`) and `topbarState()` in
|
stays). See `public/styles.css` (`topbar--*`) and `topbarState()` in
|
||||||
`public/app.js`.
|
`public/app.js`.
|
||||||
|
|
|
||||||
|
|
@ -44,7 +44,12 @@ body { display: flex; flex-direction: column; }
|
||||||
/* ---------- top-bar activity indicator (pure CSS) ----------
|
/* ---------- top-bar activity indicator (pure CSS) ----------
|
||||||
::after = dim track sitting on the bottom border
|
::after = dim track sitting on the bottom border
|
||||||
::before = the moving / solid segment that signals the state
|
::before = the moving / solid segment that signals the state
|
||||||
JS only toggles a `topbar--<state>` class. */
|
JS only toggles a `topbar--<state>` class.
|
||||||
|
|
||||||
|
All durations below are integer multiples of a 1.4s beat. Two infinite
|
||||||
|
animations with an arbitrary ratio (e.g. 4.6s vs 2.8s = 23:14) only re-align
|
||||||
|
every 64s, so they visibly drift; keeping every animation on the same beat
|
||||||
|
grid makes the pair repeat exactly and stay phase-locked. */
|
||||||
.topbar::before,
|
.topbar::before,
|
||||||
.topbar::after {
|
.topbar::after {
|
||||||
content: '';
|
content: '';
|
||||||
|
|
@ -68,28 +73,28 @@ body { display: flex; flex-direction: column; }
|
||||||
rgba(88, 166, 255, 0.13) 66%,
|
rgba(88, 166, 255, 0.13) 66%,
|
||||||
rgba(88, 166, 255, 0) 100%);
|
rgba(88, 166, 255, 0) 100%);
|
||||||
background-size: 240% 100%;
|
background-size: 240% 100%;
|
||||||
animation: topbar-sheen 4.6s linear infinite;
|
animation: topbar-sheen 5.6s linear infinite; /* 4 × beat = 2 × sweep → locked */
|
||||||
}
|
}
|
||||||
.topbar--running::after { opacity: 1; background: rgba(88, 166, 255, 0.22); }
|
.topbar--running::after { opacity: 1; background: rgba(88, 166, 255, 0.22); }
|
||||||
.topbar--running::before {
|
.topbar--running::before {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
background: linear-gradient(90deg, rgba(88, 166, 255, 0) 0%, var(--accent) 22%, #7ee787 56%, var(--green) 80%, rgba(63, 185, 80, 0) 100%);
|
background: linear-gradient(90deg, rgba(88, 166, 255, 0) 0%, var(--accent) 22%, #7ee787 56%, var(--green) 80%, rgba(63, 185, 80, 0) 100%);
|
||||||
box-shadow: 0 0 12px rgba(88, 166, 255, 0.8), 0 0 5px rgba(126, 231, 135, 0.95);
|
box-shadow: 0 0 12px rgba(88, 166, 255, 0.8), 0 0 5px rgba(126, 231, 135, 0.95);
|
||||||
animation: topbar-sweep 2.8s cubic-bezier(0.45, 0, 0.35, 1) infinite;
|
animation: topbar-sweep 2.8s cubic-bezier(0.45, 0, 0.35, 1) infinite; /* 2 × beat */
|
||||||
}
|
}
|
||||||
|
|
||||||
/* waiting for operator input — pulsing purple */
|
/* waiting for operator input — pulsing purple */
|
||||||
.topbar--waiting {
|
.topbar--waiting {
|
||||||
background-image: linear-gradient(90deg, rgba(188, 140, 255, 0.07), rgba(188, 140, 255, 0.12), rgba(188, 140, 255, 0.07));
|
background-image: linear-gradient(90deg, rgba(188, 140, 255, 0.07), rgba(188, 140, 255, 0.12), rgba(188, 140, 255, 0.07));
|
||||||
background-size: 220% 100%;
|
background-size: 220% 100%;
|
||||||
animation: topbar-sheen 2.8s linear infinite;
|
animation: topbar-sheen 2.8s linear infinite; /* 2 × beat */
|
||||||
}
|
}
|
||||||
.topbar--waiting::before {
|
.topbar--waiting::before {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
transform: none;
|
transform: none;
|
||||||
background: linear-gradient(90deg, rgba(188, 140, 255, 0.15), var(--purple), rgba(188, 140, 255, 0.15));
|
background: linear-gradient(90deg, rgba(188, 140, 255, 0.15), var(--purple), rgba(188, 140, 255, 0.15));
|
||||||
animation: topbar-breathe 1.6s ease-in-out infinite;
|
animation: topbar-breathe 1.4s ease-in-out infinite; /* 1 × beat (2 per sheen) */
|
||||||
}
|
}
|
||||||
|
|
||||||
/* paused — static amber baseline (deliberately not animating) */
|
/* paused — static amber baseline (deliberately not animating) */
|
||||||
|
|
@ -111,7 +116,7 @@ body { display: flex; flex-direction: column; }
|
||||||
/* stopped / stale-ish warnings — light amber tint */
|
/* stopped / stale-ish warnings — light amber tint */
|
||||||
.topbar--warn {
|
.topbar--warn {
|
||||||
background-image: linear-gradient(180deg, rgba(210, 153, 34, 0.13), rgba(210, 153, 34, 0.04) 72%, transparent);
|
background-image: linear-gradient(180deg, rgba(210, 153, 34, 0.13), rgba(210, 153, 34, 0.04) 72%, transparent);
|
||||||
animation: topbar-warn-pulse 2.6s ease-in-out infinite;
|
animation: topbar-warn-pulse 2.8s ease-in-out infinite; /* 2 × beat */
|
||||||
}
|
}
|
||||||
.topbar--warn::before {
|
.topbar--warn::before {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
|
|
@ -124,7 +129,7 @@ body { display: flex; flex-direction: column; }
|
||||||
/* error / done-with-errors / stale — light red tint + pulsing red line */
|
/* error / done-with-errors / stale — light red tint + pulsing red line */
|
||||||
.topbar--error {
|
.topbar--error {
|
||||||
background-image: linear-gradient(180deg, rgba(248, 81, 73, 0.17), rgba(248, 81, 73, 0.05) 74%, transparent);
|
background-image: linear-gradient(180deg, rgba(248, 81, 73, 0.17), rgba(248, 81, 73, 0.05) 74%, transparent);
|
||||||
animation: topbar-error-pulse 1.8s ease-in-out infinite;
|
animation: topbar-error-pulse 2.8s ease-in-out infinite; /* 2 × beat */
|
||||||
}
|
}
|
||||||
.topbar--error::before {
|
.topbar--error::before {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
|
|
@ -132,7 +137,7 @@ body { display: flex; flex-direction: column; }
|
||||||
transform: none;
|
transform: none;
|
||||||
background: var(--red);
|
background: var(--red);
|
||||||
box-shadow: 0 0 12px rgba(248, 81, 73, 0.8);
|
box-shadow: 0 0 12px rgba(248, 81, 73, 0.8);
|
||||||
animation: topbar-breathe 1.4s ease-in-out infinite;
|
animation: topbar-breathe 1.4s ease-in-out infinite; /* 1 × beat (2 per pulse) */
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes topbar-sweep {
|
@keyframes topbar-sweep {
|
||||||
|
|
|
||||||
19
src/goal.mjs
19
src/goal.mjs
|
|
@ -48,6 +48,19 @@ function slug(text) {
|
||||||
.slice(0, 48)
|
.slice(0, 48)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Plan authors commonly wrap a directive value in inline-code backticks, e.g.
|
||||||
|
* `:: verify: `pnpm test``. The loop passes the value to the shell verbatim, and
|
||||||
|
* a surrounding pair of backticks turns it into a command substitution — the
|
||||||
|
* gate then runs the command's *stdout* as a command (exit 127 on "Passed! …").
|
||||||
|
* Unwrap a single backtick-delimited span so the gate runs the intended command.
|
||||||
|
*/
|
||||||
|
export function unwrapCode(value) {
|
||||||
|
const text = String(value ?? '').trim()
|
||||||
|
const m = text.match(/^`([^`]*)`$/)
|
||||||
|
return m ? m[1].trim() : text
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Load a goal from --goal-text, a .json file, or a .md file with "- [ ]" checkboxes.
|
* Load a goal from --goal-text, a .json file, or a .md file with "- [ ]" checkboxes.
|
||||||
*/
|
*/
|
||||||
|
|
@ -73,8 +86,8 @@ export function loadGoal(config) {
|
||||||
title: s.title || s.name || `Stage ${i + 1}`,
|
title: s.title || s.name || `Stage ${i + 1}`,
|
||||||
details: s.details || s.description || '',
|
details: s.details || s.description || '',
|
||||||
done: Boolean(s.done),
|
done: Boolean(s.done),
|
||||||
verify: s.verify || s.verifyCommand || '',
|
verify: unwrapCode(s.verify || s.verifyCommand || ''),
|
||||||
acceptance: s.acceptance || s.acceptanceCriteria || '',
|
acceptance: unwrapCode(s.acceptance || s.acceptanceCriteria || ''),
|
||||||
model: s.model || '',
|
model: s.model || '',
|
||||||
agent: s.agent || '',
|
agent: s.agent || '',
|
||||||
variant: s.variant || '',
|
variant: s.variant || '',
|
||||||
|
|
@ -100,7 +113,7 @@ export function loadGoal(config) {
|
||||||
const directives = {}
|
const directives = {}
|
||||||
for (const part of directiveParts) {
|
for (const part of directiveParts) {
|
||||||
const dm = part.match(/^\s*(verify|acceptance)\s*:\s*(.+?)\s*$/i)
|
const dm = part.match(/^\s*(verify|acceptance)\s*:\s*(.+?)\s*$/i)
|
||||||
if (dm) directives[dm[1].toLowerCase()] = dm[2]
|
if (dm) directives[dm[1].toLowerCase()] = unwrapCode(dm[2])
|
||||||
}
|
}
|
||||||
const title = titlePart.replace(/[*_`]/g, '').trim()
|
const title = titlePart.replace(/[*_`]/g, '').trim()
|
||||||
stages.push({
|
stages.push({
|
||||||
|
|
|
||||||
|
|
@ -576,14 +576,13 @@ export class Orchestrator {
|
||||||
rec.verify = verifyResult
|
rec.verify = verifyResult
|
||||||
cls = { ...cls, verify: verifyResult }
|
cls = { ...cls, verify: verifyResult }
|
||||||
if (verifyResult.ok) {
|
if (verifyResult.ok) {
|
||||||
this.completedStages.add(stage?.id || cls.stageId)
|
this._completeStage(goal, stage, cls.stageId)
|
||||||
stageAdvanced = true
|
stageAdvanced = true
|
||||||
this.verifyFailures.delete(stage?.id || cls.stageId)
|
this.verifyFailures.delete(stage?.id || cls.stageId)
|
||||||
if (stage) this._syncGoalStage(goal, stage)
|
|
||||||
if (config.review && stage && !this.reviewedStages.has(stage.id) && !config.dryRun) {
|
if (config.review && stage && !this.reviewedStages.has(stage.id) && !config.dryRun) {
|
||||||
const review = await this._runReviewer(goal, stage, iteration)
|
const review = await this._runReviewer(goal, stage, iteration)
|
||||||
if (review?.blocked) {
|
if (review?.blocked) {
|
||||||
this.completedStages.delete(stage?.id || cls.stageId)
|
this._reopenStage(goal, stage, cls.stageId)
|
||||||
stageAdvanced = false
|
stageAdvanced = false
|
||||||
cls = {
|
cls = {
|
||||||
...cls,
|
...cls,
|
||||||
|
|
@ -615,9 +614,8 @@ export class Orchestrator {
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// No verify command: trust the agent, but it is not independently checked.
|
// No verify command: trust the agent, but it is not independently checked.
|
||||||
this.completedStages.add(stage?.id || cls.stageId)
|
this._completeStage(goal, stage, cls.stageId)
|
||||||
stageAdvanced = true
|
stageAdvanced = true
|
||||||
if (stage) this._syncGoalStage(goal, stage)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -774,6 +772,37 @@ export class Orchestrator {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mark a stage complete: record it in the in-memory goal, tick the goal file
|
||||||
|
* and publish the refreshed stage list into run state so the dashboard's
|
||||||
|
* "Stages" strip updates the moment an iteration advances — not only when a
|
||||||
|
* later run reloads the goal file from disk.
|
||||||
|
*/
|
||||||
|
_completeStage(goal, stage, fallbackId) {
|
||||||
|
const id = stage?.id || fallbackId
|
||||||
|
if (id) this.completedStages.add(id)
|
||||||
|
if (stage) stage.done = true
|
||||||
|
if (stage) this._syncGoalStage(goal, stage)
|
||||||
|
this._publishStages(goal)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Revert a stage completion (used when the reviewer blocks a stage). */
|
||||||
|
_reopenStage(goal, stage, fallbackId) {
|
||||||
|
const id = stage?.id || fallbackId
|
||||||
|
if (id) this.completedStages.delete(id)
|
||||||
|
if (stage) stage.done = false
|
||||||
|
this._publishStages(goal)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Push the live per-stage done state into run state for the UI snapshot. */
|
||||||
|
_publishStages(goal) {
|
||||||
|
const stages = (goal?.stages || []).map((s) => ({
|
||||||
|
...s,
|
||||||
|
done: Boolean(s.done || this.completedStages.has(s.id)),
|
||||||
|
}))
|
||||||
|
this.state.update({ stages })
|
||||||
|
}
|
||||||
|
|
||||||
async _runReviewer(goal, stage, iteration) {
|
async _runReviewer(goal, stage, iteration) {
|
||||||
const { config, state } = this
|
const { config, state } = this
|
||||||
const reviewPath = path.join(loopPaths(config.project).dir, 'review.json')
|
const reviewPath = path.join(loopPaths(config.project).dir, 'review.json')
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import { spawn } from 'node:child_process'
|
import { spawn } from 'node:child_process'
|
||||||
|
import { unwrapCode } from './goal.mjs'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Independent verification for a stage. The loop runs the command itself and
|
* Independent verification for a stage. The loop runs the command itself and
|
||||||
|
|
@ -13,7 +14,7 @@ const MAX_CAPTURE = 20000
|
||||||
* --verify / config.verify.
|
* --verify / config.verify.
|
||||||
*/
|
*/
|
||||||
export function verifyCommandFor(stage, config = {}) {
|
export function verifyCommandFor(stage, config = {}) {
|
||||||
const cmd = (stage?.verify || config.verify || '').trim()
|
const cmd = unwrapCode(stage?.verify || config.verify || '')
|
||||||
return cmd || null
|
return cmd || null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -40,6 +40,26 @@ test('loadGoal parses json stages', () => {
|
||||||
assert.equal(goal.stages[0].id, 'a')
|
assert.equal(goal.stages[0].id, 'a')
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test('loadGoal unwraps inline-code backticks around verify commands', () => {
|
||||||
|
const project = tmpProject()
|
||||||
|
const file = path.join(project, 'goal.md')
|
||||||
|
fs.writeFileSync(
|
||||||
|
file,
|
||||||
|
'# G\n\n- [ ] One :: verify: `pnpm test` :: acceptance: ok\n- [ ] Two :: verify: pnpm build :: acceptance: ok\n',
|
||||||
|
)
|
||||||
|
const goal = loadGoal({ project, goal: 'goal.md', goalText: '' })
|
||||||
|
assert.equal(goal.stages[0].verify, 'pnpm test')
|
||||||
|
assert.equal(goal.stages[1].verify, 'pnpm build')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('loadGoal unwraps inline-code backticks in json verify', () => {
|
||||||
|
const project = tmpProject()
|
||||||
|
const file = path.join(project, 'goal.json')
|
||||||
|
fs.writeFileSync(file, JSON.stringify({ title: 'T', stages: [{ id: 'a', title: 'A', verify: '`pnpm test`' }] }))
|
||||||
|
const goal = loadGoal({ project, goal: 'goal.json', goalText: '' })
|
||||||
|
assert.equal(goal.stages[0].verify, 'pnpm test')
|
||||||
|
})
|
||||||
|
|
||||||
test('loadGoal falls back to a single stage for inline text', () => {
|
test('loadGoal falls back to a single stage for inline text', () => {
|
||||||
const goal = loadGoal({ project: '/tmp', goal: '', goalText: 'Do the thing' })
|
const goal = loadGoal({ project: '/tmp', goal: '', goalText: 'Do the thing' })
|
||||||
assert.equal(goal.stages.length, 1)
|
assert.equal(goal.stages.length, 1)
|
||||||
|
|
|
||||||
116
test/orchestrator-stages.test.mjs
Normal file
116
test/orchestrator-stages.test.mjs
Normal file
|
|
@ -0,0 +1,116 @@
|
||||||
|
import { test } from 'node:test'
|
||||||
|
import assert from 'node:assert/strict'
|
||||||
|
import fs from 'node:fs'
|
||||||
|
import os from 'node:os'
|
||||||
|
import path from 'node:path'
|
||||||
|
|
||||||
|
import { Orchestrator } from '../src/orchestrator.mjs'
|
||||||
|
import { loopPaths } from '../src/goal.mjs'
|
||||||
|
|
||||||
|
const sink = { write: () => true }
|
||||||
|
|
||||||
|
function makeProject(stages) {
|
||||||
|
const project = fs.mkdtempSync(path.join(os.tmpdir(), 'kilo-stages-'))
|
||||||
|
fs.mkdirSync(path.join(project, '.kilocode-loop'), { recursive: true })
|
||||||
|
fs.writeFileSync(path.join(project, 'goal.json'), JSON.stringify({ title: 'Test goal', stages }))
|
||||||
|
return project
|
||||||
|
}
|
||||||
|
|
||||||
|
function makeConfig(project, overrides = {}) {
|
||||||
|
return {
|
||||||
|
project,
|
||||||
|
goal: 'goal.json',
|
||||||
|
goalText: '',
|
||||||
|
iterations: 3,
|
||||||
|
agent: 'code-design',
|
||||||
|
model: '',
|
||||||
|
variant: '',
|
||||||
|
thinking: false,
|
||||||
|
auto: true,
|
||||||
|
sharedContext: false,
|
||||||
|
hitl: 'off',
|
||||||
|
confirm: false,
|
||||||
|
port: 7997,
|
||||||
|
host: '127.0.0.1',
|
||||||
|
runId: 'stages-run',
|
||||||
|
dryRun: false,
|
||||||
|
keepOpen: false,
|
||||||
|
quiet: true,
|
||||||
|
color: false,
|
||||||
|
promptExtra: '',
|
||||||
|
maxIterationMinutes: 0,
|
||||||
|
contextWarnTokens: 150000,
|
||||||
|
verify: '',
|
||||||
|
verifyTimeoutMinutes: 30,
|
||||||
|
maxCost: 0,
|
||||||
|
maxTokens: 0,
|
||||||
|
maxStaleIterations: 0,
|
||||||
|
checkpoint: false,
|
||||||
|
guard: true,
|
||||||
|
redact: true,
|
||||||
|
review: false,
|
||||||
|
reviewAgent: '',
|
||||||
|
autoFreshTokens: 0,
|
||||||
|
notify: '',
|
||||||
|
syncGoal: true,
|
||||||
|
...overrides,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deterministic runner that reports its current stage as done on every
|
||||||
|
* iteration, so the loop advances one stage per iteration.
|
||||||
|
*/
|
||||||
|
async function alwaysDoneRunner({ config, prompt }) {
|
||||||
|
const iteration = Number(prompt.match(/iteration (\d+)\//)?.[1] || 1)
|
||||||
|
fs.writeFileSync(
|
||||||
|
loopPaths(config.project).progress,
|
||||||
|
JSON.stringify({
|
||||||
|
iteration,
|
||||||
|
topic: `advance ${iteration}`,
|
||||||
|
stageId: `reported-${iteration}`,
|
||||||
|
stageStatus: 'done',
|
||||||
|
percent: iteration * 10,
|
||||||
|
updatedAt: new Date().toISOString(),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
return {
|
||||||
|
sessionID: `ses_test_${iteration}`,
|
||||||
|
exitCode: 0,
|
||||||
|
error: null,
|
||||||
|
interrupted: false,
|
||||||
|
durationMs: 1,
|
||||||
|
texts: [`iteration ${iteration} done`],
|
||||||
|
usage: { input: 1, output: 1, reasoning: 0, cacheRead: 0, cacheWrite: 0, total: 2 },
|
||||||
|
cost: 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
test('live stage progress reaches run state (dashboard "Stages" strip)', async () => {
|
||||||
|
const project = makeProject([
|
||||||
|
{ id: 's1', title: 'One' },
|
||||||
|
{ id: 's2', title: 'Two' },
|
||||||
|
{ id: 's3', title: 'Three' },
|
||||||
|
])
|
||||||
|
const final = await new Orchestrator(makeConfig(project, { runner: alwaysDoneRunner }), { out: sink }).run()
|
||||||
|
|
||||||
|
assert.equal(final.status, 'done')
|
||||||
|
assert.deepEqual(
|
||||||
|
final.stages.map((s) => s.done),
|
||||||
|
[true, true, true],
|
||||||
|
'every completed stage must be reported as done in run state',
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('a single-iteration advance is published before the next iteration starts', async () => {
|
||||||
|
const project = makeProject([
|
||||||
|
{ id: 's1', title: 'One' },
|
||||||
|
{ id: 's2', title: 'Two' },
|
||||||
|
])
|
||||||
|
const orchestrator = new Orchestrator(makeConfig(project, { iterations: 1, runner: alwaysDoneRunner }), { out: sink })
|
||||||
|
const seen = []
|
||||||
|
orchestrator.onReport = () => seen.push(orchestrator.state.state.stages.map((s) => s.done))
|
||||||
|
await orchestrator.run()
|
||||||
|
|
||||||
|
assert.deepEqual(seen[0], [true, false], 'the first stage is done in run state right after iteration 1')
|
||||||
|
})
|
||||||
|
|
@ -9,6 +9,15 @@ test('verifyCommandFor prefers the stage command over the global default', () =>
|
||||||
assert.equal(verifyCommandFor({}, {}), null)
|
assert.equal(verifyCommandFor({}, {}), null)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test('verifyCommandFor unwraps inline-code backticks from a verify value', () => {
|
||||||
|
// `:: verify: \`pnpm test\`` must run `pnpm test`, not a command substitution
|
||||||
|
// whose stdout ("Test run for …") is then executed as a command (exit 127).
|
||||||
|
assert.equal(verifyCommandFor({ verify: '`pnpm test`' }, {}), 'pnpm test')
|
||||||
|
assert.equal(verifyCommandFor({}, { verify: '`node --test`' }), 'node --test')
|
||||||
|
// An inner pair of backticks is legitimate shell syntax: leave it alone.
|
||||||
|
assert.equal(verifyCommandFor({ verify: 'echo "`date`"' }, {}), 'echo "`date`"')
|
||||||
|
})
|
||||||
|
|
||||||
test('runVerify reports success and captures output', async () => {
|
test('runVerify reports success and captures output', async () => {
|
||||||
const res = await runVerify({ command: 'echo hello-verify', cwd: process.cwd() })
|
const res = await runVerify({ command: 'echo hello-verify', cwd: process.cwd() })
|
||||||
assert.equal(res.ok, true)
|
assert.equal(res.ok, true)
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue