diff --git a/trios/agent-server/apps/agent/lib/schedules/scheduleStorage.test.ts b/trios/agent-server/apps/agent/lib/schedules/scheduleStorage.test.ts index 5b192ee400..49ad9809c1 100644 --- a/trios/agent-server/apps/agent/lib/schedules/scheduleStorage.test.ts +++ b/trios/agent-server/apps/agent/lib/schedules/scheduleStorage.test.ts @@ -1225,3 +1225,119 @@ describe('scheduleResultsContract', () => { throw new Error('the dialog must take the retry handler') }) }) + +// --------------------------------------------------------------------------- +// The scheduled task card (gHashTag/trios#1550), appended to this file by +// the same readdir rule as the two blocks above: its subject imports this +// suite's subject (the store, through useScheduledJobRuns), whose module +// this file already materialized real behind its own stand-ins - a +// standalone suite in the subject's group would link the store fresh and +// trip the generated-graphql document the local run does not carry. +// +// The card is held as a FULL SOURCE CONTRACT, no render: its graph also +// reaches the llm-providers store, whose own suite (lib/llm-providers/ +// storage.test.ts) pins the defineItem interception of ITS OWN storage +// seam, recorded at that module's first materialization - and bun links +// and runs test files one by one, so whichever order the two files meet +// in, a card import from here materializes the providers store first (or +// replaces it) and poisons that suite's pins. Measured both directions +// on the first run of this block; the card's own observable answer is +// effect-gated either way (the run list arrives through effects a static +// render never runs), so the source checks below are the contract. + +const taskCardCode = () => + fs + .readFileSync( + path.join( + path.dirname(fileURLToPath(import.meta.url)), + '../../entrypoints/app/scheduled-tasks/ScheduledTaskCard.tsx', + ), + 'utf8', + ) + .split('\n') + .filter((l) => !/^\s*(\/\/|\*|\/\*)/.test(l)) + .join('\n') + +describe('scheduledTaskCardContract', () => { + it('carries the switch on the enabled flag with its Disable/Enable label, the name and quoted query, and the Disabled badge gated on the disabled flag', () => { + const src = taskCardCode() + if (!/checked=\{job\.enabled\}/.test(src)) + throw new Error('the switch must ride the enabled flag') + if (!/onCheckedChange=\{onToggle\}/.test(src)) + throw new Error('the switch must keep its toggle handler') + if ( + !/aria-label=\{`\$\{job\.enabled \? 'Disable' : 'Enable'\} \$\{job\.name\}`\}/.test( + src, + ) + ) + throw new Error('the switch label must keep its enabled-branch naming') + if (!/font-semibold">\{job\.name\}<\/span>/.test(src)) + throw new Error('the card must carry the job name') + if (!/"\{job\.query\}"/.test(src)) + throw new Error('the card must carry the quoted query') + if (!/\{!job\.enabled && \(/.test(src)) + throw new Error('the Disabled badge must be gated on the disabled flag') + if (!/\n\s+Disabled\n/.test(src)) + throw new Error('the Disabled badge must keep its label') + }) + + it('gates the provider and last-run rows on their loaded values, and carries the Test and Edit buttons with the delete behind its label', () => { + const src = taskCardCode() + if (!/\{providerInfo && \(/.test(src)) + throw new Error('the provider row must stay gated on the loaded info') + if (!/\{job\.lastRunAt && \(/.test(src)) + throw new Error('the last-run row must stay gated on the timestamp') + if (!/Last run: \{formatRelativeTime\(job\.lastRunAt\)\}/.test(src)) + throw new Error('the last-run row must keep its relative line') + if (!/aria-label=\{`Delete \$\{job\.name\}`\}/.test(src)) + throw new Error('the delete control must keep its name label') + if (!/onClick=\{onRun\}/.test(src) || !/onClick=\{onEdit\}/.test(src)) + throw new Error('the Test and Edit buttons must keep their handlers') + if (!/\n\s+Test\n/.test(src) || !/\n\s+Edit\n/.test(src)) + throw new Error('the Test and Edit buttons must keep their labels') + if (!/ { + const src = taskCardCode() + if (!/`Daily at \$\{job\.scheduleTime\}`/.test(src)) + throw new Error('the daily schedule must keep its time line') + if (!/'Every hour'/.test(src)) + throw new Error('the single-hour interval must keep its line') + if (!/`Every \$\{job\.scheduleInterval\} hours`/.test(src)) + throw new Error('the hourly interval must keep its line') + if (!/'Every minute'/.test(src)) + throw new Error('the single-minute interval must keep its line') + if (!/`Every \$\{job\.scheduleInterval\} minutes`/.test(src)) + throw new Error('the minute interval must keep its line') + if (!/'Not scheduled'/.test(src)) + throw new Error('the unset schedule must keep its fallback line') + if (!/if \(!completedAt\) return 'Running\.\.\.'/.test(src)) + throw new Error('the open run must keep its running note') + if (!/`\$\{mins\}m \$\{secs\}s`/.test(src)) + throw new Error('the long durations must keep their minute-second split') + }) + + it('gates the run history rows on the measured statuses behind their icons and labels, keeps the View row action and the counted title', () => { + const src = taskCardCode() + if (!/Run History \(\{runs\.length\}\)/.test(src)) + throw new Error('the run history must keep its counted title') + if (!/run\.status === 'completed' \? \(/.test(src)) + throw new Error('the completed runs must keep their icon branch') + if (!/run\.status === 'failed' \? \(/.test(src)) + throw new Error('the failed runs must keep their icon branch') + if (!/