Finley ZhouA free server will not tell you when it skips your job. It will simply not run it. That is not a bug...
A free server will not tell you when it skips your job. It will simply not run it. That is not a bug in the platform; it is the contract of the tier. The only sane response is to design the job so a skipped run costs nothing.
This case study covers one small project end to end: a C++17 config validator, drafted with a free model, hardened with a spec table, and deployed as a nightly cron job on MonkeyCode's free server option. The spec table caught five defects the unit tests missed. The job survived skipped runs because it was designed to be disposable.
A legacy build script read a hand-maintained INI file. Misspelled keys did not fail. They fell back to defaults, and the build produced an artifact nobody noticed was wrong.
The goal was a small validator that reads the file, checks it against a schema, and exits non-zero with sorted diagnostics. Constraints: C++17, one static binary, no third-party dependencies, and a hard runtime budget of one second per megabyte. The tool had to be boring. It also had to run somewhere free, because the point was to catch a slow, silent failure mode — not to justify a new paid service.
I generated the first draft with MonkeyCode's free model access. Disclosure: This article was prepared as part of MonkeyCode's product outreach.
The draft parsed sections, keys, and values, and it passed the unit tests I wrote while reading the code. That was the trap. Unit tests written from a model's code inherit the model's assumptions about the input format. The real file format had three details the draft got wrong, and none of my unit tests touched them.
I wrote a spec table from the actual file format, not from the code. Thirty-one cases in six categories:
| # | Category | Cases | What it guards |
|---|---|---|---|
| 1 | Section syntax | 5 | unterminated [, empty name, inline comment after ], whitespace, duplicate section |
| 2 | Key syntax | 5 | empty key, leading digit, illegal character, spaces around =, duplicate key |
| 3 | Values | 6 | quoted value, ; inside quotes, empty value, = inside value, trailing whitespace, unquoted ;
|
| 4 | Comments | 4 |
; at line start, inline ;, ; after quoted value, comment-only line |
| 5 | Line endings | 4 | LF, CRLF, lone CR, blank lines |
| 6 | Schema | 7 | missing required key, wrong type, out-of-range value, unknown key, case sensitivity, default fallback, empty section |
Round 1 failed five of the 31 cases: two CRLF cases, two quoted-semicolon cases, and one duplicate-section case. The unit tests still passed. Round 2 fixed the parsing but introduced a contract violation: the validator used exit code 1 for both "config invalid" and "internal error".
The final parser core is unremarkable on purpose:
std::vector<Diagnostic> parse(std::istream& in, std::vector<Entry>& out) {
std::vector<Diagnostic> diags;
std::string section, raw;
int line_no = 0;
while (std::getline(in, raw)) {
++line_no;
std::string line = trim(raw); // CRLF -> LF
if (line.empty() || line[0] == ';') continue;
if (line[0] == '[') {
auto close = line.find(']');
if (close == std::string::npos) {
diags.push_back({line_no, "unterminated section"});
continue;
}
section = trim(line.substr(1, close - 1));
if (section.empty()) diags.push_back({line_no, "empty section name"});
continue;
}
auto eq = line.find('=');
if (eq == std::string::npos) {
diags.push_back({line_no, "expected '='"});
continue;
}
std::string key = trim(line.substr(0, eq));
std::string value = trim(line.substr(eq + 1));
if (!is_valid_key(key)) {
diags.push_back({line_no, "invalid key: " + key});
continue;
}
// strip an inline comment only when it is outside quotes
if (value.size() >= 2 && value.front() == '"' && value.back() == '"') {
value = value.substr(1, value.size() - 2);
} else if (auto c = value.find(';'); c != std::string::npos) {
value = trim(value.substr(0, c));
}
out.push_back({section, key, value, line_no});
}
return diags;
}
The interesting part is the exit-code contract, written into the repo before the code:
0 — config is valid1 — config is invalid; diagnostics on stdout2 — the validator itself failedRound 3 passed all 31 spec cases and honored the contract. Schema checks — required keys, types, ranges — live in a separate function that consumes the parsed entries, so the parser stays a pure syntax layer.
The free server option was the right place for this job for one reason: the job is disposable. It runs nightly, holds no state, and a missed run costs nothing because the next run re-checks the whole file.
Four design rules made that true:
g++ -std=c++17 -O2 -static cfgcheck.cpp -o cfgcheck — no shared libraries, no runtime to install.The wrapper is deliberately boring:
#!/usr/bin/env bash
set -uo pipefail
BIN=/opt/cfgcheck/cfgcheck
CFG=/srv/build/config.ini
LOG=/var/log/cfgcheck.log
"$BIN" "$CFG" >>"$LOG" 2>&1
code=$?
if [[ $code -eq 1 ]]; then
echo "[$(date -u +%FT%TZ)] config invalid:" >>"$LOG"
"$BIN" "$CFG" 2>&1 | head -20 >>"$LOG"
fi
exit 0
The wrapper exits 0 on purpose. The log is the artifact, not the exit code. A second cron line, five minutes later, checks the log's mtime and fails if the job never ran:
# crontab
0 3 * * * /opt/cfgcheck/nightly-cfgcheck.sh
5 3 * * * test -n "$(find /var/log/cfgcheck.log -mmin -60)" || echo "cfgcheck missed its run" >>/var/log/cfgcheck.missed
That separation matters. The validator's exit code describes the config file; the freshness check describes the infrastructure. Mixing the two is how a skipped job becomes a silent one.
I used this table to decide the free server was acceptable:
| Property | Free server is fine | Free server is not enough |
|---|---|---|
| State | stateless, idempotent | stateful sessions, queues |
| Missed run | rerun later, no harm | data loss, missed deadline |
| Latency | minutes acceptable | user-facing request path |
| Alerting | log file + mtime check | must page someone in seconds |
| Secrets | none | API keys, customer data |
The spec table caught three defect classes the unit tests never touched:
std::getline keeps the \r. The first draft compared "true" against "true\r" and reported a schema violation on every Windows-edited file.url = "https://x/a;b" — the first draft stripped everything after ;, producing a truncated URL that still looked plausible.[build] blocks silently. The spec table required an error.| Round | Unit tests | Spec table | Exit-code contract |
|---|---|---|---|
| 1 | pass | 5 failures | missing |
| 2 | pass | 2 failures | wrong codes |
| 3 | pass | 0 failures | correct |
Runtime on a 1MB file stayed under 20ms in my environment; the code above is complete enough to reproduce that measurement. The deployment ran without a false alarm. The freshness check is what makes a skipped run visible instead of silent.
The spec table is the contract; the model is a fast typist. Unit tests written from the model's code inherit the model's assumptions. The spec table comes from the file format — from the real world — and that is where its power comes from.
Free infrastructure rewards disposable jobs. If a skipped run is harmless, a free server is enough. The design work is not in the code; it is in making the job stateless, idempotent, and bounded.
Exit codes are the API. The model's first version printed "ERROR" and returned 0. The human invariant — 0, 1, 2, documented in the repo — is what the cron wrapper depends on. Without it, the wrapper is guessing.
This approach is not for everyone. The spec table is not a fuzzer; the config file is trusted input here, and if your file comes from users, add a fuzzing gate before relying on this validator. If a missed run is unacceptable, or if the job must alert someone within minutes, a free server is the wrong tool. And the numbers above are specific to this file format — copy the method, not the cases.
If you are about to paste a free model's draft into CI, write the spec table first. It takes an hour, and it will find the defects your unit tests were written to miss.