mirror of
https://github.com/awizemann/scarf.git
synced 2026-05-10 10:36:35 +00:00
feat(templates): upgrade site-status-checker to v1.1.0 with config schema
First real exercise of the v2.3 configuration feature. The template no longer asks the agent to bootstrap sites.txt on first run — instead, users enter their list of URLs through the Configure form during install, and change them later via the dashboard's Configuration button. This makes the template a complete round-trip test of the new feature end-to-end. Schema (manifest.config.schema): - `sites` — list<string>, required, 1–25 items, default two example URLs. This is the list the cron job hits. - `timeout_seconds` — number, 1–60, default 10. Per-URL HTTP timeout. - `modelRecommendation.preferred = claude-haiku-4` — rationale: simple tool-use task, Haiku is cost-effective for daily cron. Manifest bumped: schemaVersion 1 → 2, version 1.0.0 → 1.1.0, minScarfVersion 2.2.0 → 2.3.0, contents.config = 2. AGENTS.md rewritten for the config-driven flow: - Reads values from `.scarf/config.json` at run time (values.sites + values.timeout_seconds). No more sites.txt bootstrap. - "Add a site" / "Remove a site" no longer mean the agent edits a file — they mean "open the Configuration button on the dashboard." The agent points the user there rather than trying to mutate config.json itself. A future Scarf release may expose a tool for agents to write config programmatically; until then, config is strictly a user action. - First-run bootstrap now only creates status-log.md (if absent). README.md rewritten to walk users through the new form-based flow, explain the Configuration button, and document the model recommendation. Uninstall instructions point at the right-click Uninstall Template action rather than manual steps. Cron prompt updated to reference config.json (values.sites, values.timeout_seconds) instead of sites.txt. ProjectTemplateExampleTemplateTests.siteStatusCheckerParsesAndPlans extended with v2-specific assertions: manifest.schemaVersion == 2, contents.config == 2, schema.fields.count == 2, per-field constraints (sites type/itemType/minItems/maxItems, timeout min/max), modelRecommendation.preferred, plan.configSchema + plan.manifestCachePath are populated, plan.projectFiles includes both config.json + manifest.json destinations. Cron-prompt assertion swapped from sites.txt to config.json/values.sites. Three suites that touch ~/.hermes/scarf/projects.json now carry .serialized — the new Phase B install-with-config tests stressed the parallel-execution race in the snapshot/restore helpers. Serializing within each suite deflakes without any architectural change. Swift 50/50, Python 24/24, catalog validator accepts the upgraded bundle. Site detail page now has manifest.json for renderConfigSchema to pick up. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -253,7 +253,7 @@ import Foundation
|
||||
/// are exhaustively tested; global-state side effects (skills namespace,
|
||||
/// cron CLI, memory append) are covered by manual verification per the
|
||||
/// plan's step 7.
|
||||
@Suite struct ProjectTemplateInstallerTests {
|
||||
@Suite(.serialized) struct ProjectTemplateInstallerTests {
|
||||
|
||||
@Test func installsMinimalBundleAndWritesLockFile() throws {
|
||||
let scratch = try ProjectTemplateServiceTests.makeTempDir()
|
||||
@@ -370,7 +370,7 @@ import Foundation
|
||||
/// it, verify every tracked file is gone, the registry is restored to its
|
||||
/// pre-install state, and user-added files (if any) are preserved. Scoped
|
||||
/// to bundles with no skills/cron/memory so no global state is touched.
|
||||
@Suite struct ProjectTemplateUninstallerTests {
|
||||
@Suite(.serialized) struct ProjectTemplateUninstallerTests {
|
||||
|
||||
@Test func roundTripsInstallThenUninstall() throws {
|
||||
let scratch = try ProjectTemplateServiceTests.makeTempDir()
|
||||
@@ -496,7 +496,7 @@ import Foundation
|
||||
/// against a synthesized schemaful bundle. Uses an isolated Keychain
|
||||
/// service suffix so no leftover login-Keychain items remain after the
|
||||
/// test — every secret we write is deleted on teardown.
|
||||
@Suite struct ProjectTemplateConfigInstallTests {
|
||||
@Suite(.serialized) struct ProjectTemplateConfigInstallTests {
|
||||
|
||||
/// Minimal schemaful manifest with one non-secret field + one
|
||||
/// secret field. Written into the synthesized `.scarftemplate`
|
||||
@@ -781,13 +781,31 @@ import Foundation
|
||||
defer { service.cleanupTempDir(inspection.unpackedDir) }
|
||||
|
||||
#expect(inspection.manifest.id == "awizemann/site-status-checker")
|
||||
#expect(inspection.manifest.schemaVersion == 2) // config-enabled
|
||||
#expect(inspection.manifest.contents.dashboard)
|
||||
#expect(inspection.manifest.contents.agentsMd)
|
||||
#expect(inspection.manifest.contents.cron == 1)
|
||||
#expect(inspection.manifest.contents.config == 2)
|
||||
#expect(inspection.cronJobs.count == 1)
|
||||
#expect(inspection.cronJobs.first?.name == "Check site status")
|
||||
#expect(inspection.cronJobs.first?.schedule == "0 9 * * *")
|
||||
|
||||
// Schema assertions — the two fields we declared should survive
|
||||
// unzip + parse + validate with their constraints intact.
|
||||
let schema = try #require(inspection.manifest.config)
|
||||
#expect(schema.fields.count == 2)
|
||||
let sitesField = try #require(schema.field(for: "sites"))
|
||||
#expect(sitesField.type == .list)
|
||||
#expect(sitesField.itemType == "string")
|
||||
#expect(sitesField.required == true)
|
||||
#expect(sitesField.minItems == 1)
|
||||
#expect(sitesField.maxItems == 25)
|
||||
let timeoutField = try #require(schema.field(for: "timeout_seconds"))
|
||||
#expect(timeoutField.type == .number)
|
||||
#expect(timeoutField.minNumber == 1)
|
||||
#expect(timeoutField.maxNumber == 60)
|
||||
#expect(schema.modelRecommendation?.preferred == "claude-haiku-4")
|
||||
|
||||
let scratch = try ProjectTemplateServiceTests.makeTempDir()
|
||||
defer { try? FileManager.default.removeItem(atPath: scratch) }
|
||||
let plan = try service.buildPlan(inspection: inspection, parentDir: scratch)
|
||||
@@ -795,6 +813,12 @@ import Foundation
|
||||
#expect(plan.skillsFiles.isEmpty)
|
||||
#expect(plan.memoryAppendix == nil)
|
||||
#expect(plan.cronJobs.count == 1)
|
||||
#expect(plan.configSchema?.fields.count == 2)
|
||||
#expect(plan.manifestCachePath?.hasSuffix("/.scarf/manifest.json") == true)
|
||||
// Plan queues both config.json + manifest.json in projectFiles.
|
||||
let destinations = plan.projectFiles.map(\.destinationPath)
|
||||
#expect(destinations.contains { $0.hasSuffix("/.scarf/config.json") })
|
||||
#expect(destinations.contains { $0.hasSuffix("/.scarf/manifest.json") })
|
||||
// Cron job name gets prefixed with the template tag so users can
|
||||
// find + remove it later.
|
||||
#expect(plan.cronJobs.first?.name == "[tmpl:awizemann/site-status-checker] Check site status")
|
||||
@@ -820,10 +844,13 @@ import Foundation
|
||||
#expect(statTitles.contains("Sites Down"))
|
||||
#expect(statTitles.contains("Last Checked"))
|
||||
|
||||
// The cron prompt mentions sites.txt and dashboard.json — if it
|
||||
// ever stops doing that, the agent won't know what files to touch.
|
||||
// Cron prompt references .scarf/config.json (where values.sites
|
||||
// + values.timeout_seconds live) and the dashboard/log it writes.
|
||||
// If either stops being referenced, the cron wouldn't know which
|
||||
// data to read or where to write results.
|
||||
let cronPrompt = inspection.cronJobs.first?.prompt ?? ""
|
||||
#expect(cronPrompt.contains("sites.txt"))
|
||||
#expect(cronPrompt.contains("config.json"))
|
||||
#expect(cronPrompt.contains("values.sites"))
|
||||
#expect(cronPrompt.contains("dashboard.json"))
|
||||
#expect(cronPrompt.contains("status-log.md"))
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user