Compare commits

...

2 commits

Author SHA1 Message Date
fy59
cc87f26b98 docs(sync): record native candidate evidence 2026-09-17 15:26:14 +02:00
fy59
2c5a8be282 fix(sync): reject truncated native artifacts 2026-09-17 15:19:59 +02:00
13 changed files with 943 additions and 465 deletions

View file

@ -2,6 +2,12 @@
## Unreleased — 0.1.2
- Closed the native candidate warning gate with checked path and HTTP response
bounds, strict GCC and Clang builds, full Clang ASan/UBSan validation, and a
stable concurrent object-I/O MTP double. Rebuilt the desktop, current Android
and non-installed schema-v17 bridge candidates with recorded digests; no
service, device, user database, signing secret or deployment was touched.
- Normalized the recent synchronization, generation-MTP, Android backup,
Web Dashboard serialization, tests, I/O doubles and packaging code for human
readability. Added scoped C formatting rules and review guidance without

View file

@ -8,6 +8,13 @@ non-installed schema-v17 bridge candidate is reproducibly built and tested.
Physical-device MTP, release signing, real user backup/restore, main integration
and deployment are not complete.
`TRAINLOG_NATIVE_BUILD_AND_CANDIDATE_V1` closes the native warning gate. Strict
GCC 16.2.1 and Clang 22.1.8 builds now pass the same 81-test inventory, and the
Clang ASan/UBSan build passes it with leak detection. Bounded path assembly and
the Web sync accepted response reject insufficient capacity without publishing
partial output. Fresh desktop, current-Android and schema-v17 bridge candidates
were rebuilt from commit `2c5a8be2827e31fc5be2accb65073bfeb79d6068`.
Snapshot date: **2026-09-17**.
`TRAINLOG_CODE_READABILITY_V1` normalizes the recent generation-MTP,

View file

@ -49,6 +49,18 @@ and non-destructive hardware smoke. Drive remains separate and untested here.
This is a non-executed rollout checklist. The development branch implements the
path; the daily installation still uses V3.
The latest software-only candidate set was rebuilt under the private root
`/home/fy59/.cache/trainlog/native-build-candidate-v1.VbFjE9` from source commit
`2c5a8be2827e31fc5be2accb65073bfeb79d6068`. Its desktop inventory records every
packaged file digest plus schemas, protocols, entry points and runtime
dependencies. The current debug APK SHA-256 is
`aca2f499ee9ff53ba5913a76a2a0117274e3e5545967ce361f6f126a6af497e1`; the
non-installed bridge APK SHA-256 is
`0a28d81cbb0818cdf2fee72d5024a4b706fcfda39f1182c682c12f000a026cb4`; and its
synthetic schema-v17 backup SHA-256 is
`06eb3999c6f3ce2fd8f551b38727a0a67fe6b48dde4c3244b3d50357afa7e6a3`.
These are unsigned development evidence, not deployment authorization.
## Compatible set
- desktop binary containing schema v21, the Web API and the generation worker;

View file

@ -1317,12 +1317,14 @@ behavioral assertions. Python syntax/orchestrator tests, Android generation and
backup tests, the I/O-boundary MTP browser proof, JSON/import validators and
the isolated harness cover the other normalized sources.
GCC 16.2.1 now compiles beyond the former `misleading-indentation` failures in
`json_writer.c`, `web_dashboard_json.c`, `web_dashboard.c`,
`dashboard_layout.c`, and `generation_mtp.c`. The separate strict build remains
blocked by distinct pre-existing `format-truncation` diagnostics in the Web
accepted-response buffer and bounded test path buffers. Warning policy remains
unchanged; GCC is not reported as green.
GCC 16.2.1 and Clang 22.1.8 both pass separate strict C17 builds and the full
**81/81** Meson inventory with `werror=true` and warning level 3. The former GCC
`format-truncation` findings are closed by exact checked path construction in
the generation-MTP tests/double and by a production accepted-response
serializer that reports its byte count. Boundary tests cover exact fit,
one-byte insufficiency and multibyte path components; failure leaves no partial
path or response available to a caller. Clang ASan/UBSan passes the same
**81/81** inventory with leak detection.
The readability closeout preserves the **81/81** normal Meson and **81/81**
ASan/UBSan inventories. The isolated Android run reports **220 tests: 215
@ -1330,3 +1332,14 @@ passed, five skipped, zero failures/errors**, and `assembleDebug` succeeds on
JDK 17. The real Firefox/object-I/O-boundary MTP conversation also passes after
formatting. A source comparison against the pre-readability commit confirms
that the scoped test names and assertion counts are unchanged.
The isolated JDK 17 harness reports **220 Android tests: 215 passed, five
skipped, zero failures/errors**. The skips are optional, externally supplied
copy gates: `BridgeBackupMigrationTest.verifiedSchemaSeventeenBridgeBackupMigratesThroughCurrentOwner`,
`RealAndroidV9BodyZonesMigrationTest.realVersionNineCopyMigratesWithoutChangingExistingTables`,
`RealAndroidV14FeedbackRevisionMigrationTest.freshRealCopyMigratesLosslesslyToVersionFifteen`,
and the version-thirteen and version-twelve methods in
`RealAndroidV12MachineExerciseMigrationTest`. They require respectively the
bridge backup input, a real v9 fixture, a real v14 fixture/output directory,
and real v13/v12 fixtures with explicit output directories. Synthetic bridge
and current-schema migration proofs run separately; no user database is read.

View file

@ -5,8 +5,11 @@ import json, os, signal, socket, sqlite3, subprocess, tempfile, time, unittest
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
BUILD = ROOT / os.environ.get("TRAINLOG_WEB_E2E_BUILD", "build") / "tui"
GECKO = Path(
os.environ.get("TRAINLOG_GECKODRIVER", "/home/fy59/.cache/trainlog/web-e2e-tools/geckodriver")
os.environ.get(
"TRAINLOG_GECKODRIVER", "/home/fy59/.cache/trainlog/web-e2e-tools/geckodriver"
)
)
@ -39,7 +42,9 @@ class BrowserSyncTest(unittest.TestCase):
transport.mkdir()
owned = root / "owned"
owned.mkdir()
remote = (root / "device/Documents/Trainlog") if mode == "mtp" else transport
remote = (
(root / "device/Documents/Trainlog") if mode == "mtp" else transport
)
remote.mkdir(parents=True, exist_ok=True)
release = root / "release-android"
android_log = (root / "android.log").open("wb")
@ -114,13 +119,13 @@ class BrowserSyncTest(unittest.TestCase):
env.update(
{
"TRAINLOG_SYNC_MTP_ADAPTER": str(
ROOT / "build/tui/generation-mtp-adapter-double"
BUILD / "generation-mtp-adapter-double"
),
"TRAINLOG_MTP_DOUBLE_ROOT": str(root / "device"),
}
)
server = subprocess.Popen(
[str(ROOT / "build/tui/trainlog"), "--web", "--port", str(port)],
[str(BUILD / "trainlog"), "--web", "--port", str(port)],
env=env,
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
@ -139,11 +144,15 @@ class BrowserSyncTest(unittest.TestCase):
driver.get(f"http://127.0.0.1:{port}/")
wait = WebDriverWait(driver, 15)
button = wait.until(
lambda d: d.find_element(By.XPATH, "//button[normalize-space()='Synchroniser']")
lambda d: d.find_element(
By.XPATH, "//button[normalize-space()='Synchroniser']"
)
)
wait.until(lambda _: button.is_enabled())
button.click()
state_path = Path(str(xdg / "trainlog" / "trainlog.db") + ".sync-run.json")
state_path = Path(
str(xdg / "trainlog" / "trainlog.db") + ".sync-run.json"
)
observed = await_condition(
lambda: (
json.loads(state_path.read_text())
@ -163,7 +172,10 @@ class BrowserSyncTest(unittest.TestCase):
self.assertEqual(committed["result"], "running")
with sqlite3.connect(xdg / "trainlog" / "trainlog.db") as database:
self.assertEqual(
database.execute("SELECT COUNT(*) FROM execution_drafts").fetchone()[0], 1
database.execute(
"SELECT COUNT(*) FROM execution_drafts"
).fetchone()[0],
1,
)
release.touch()
final = await_condition(
@ -185,16 +197,21 @@ class BrowserSyncTest(unittest.TestCase):
)
summary.click()
wait.until(
lambda d: "Brouillons reçus" in d.page_source and "active" in d.page_source
lambda d: "Brouillons reçus" in d.page_source
and "active" in d.page_source
)
driver.refresh()
wait.until(lambda d: "Synchronisation confirmée" in d.page_source)
state = json.loads(
Path(str(xdg / "trainlog" / "trainlog.db") + ".sync-run.json").read_text()
Path(
str(xdg / "trainlog" / "trainlog.db") + ".sync-run.json"
).read_text()
)
self.assertEqual(state["phase"], "completed")
self.assertEqual(
android.wait(timeout=30), 0, (root / "android.log").read_text(errors="replace")
android.wait(timeout=30),
0,
(root / "android.log").read_text(errors="replace"),
)
finally:
if driver is not None:

View file

@ -72,7 +72,11 @@ def run_adapter(
diagnostic = (
result.stderr.decode("utf-8", "replace")[:1024] or "MTP adapter failed"
).strip()
if allow_missing_peer and "expected peer was not found" in diagnostic:
missing_peer = diagnostic in {
"double failed 6 expected Android peer not found",
"MTP adapter failed status=6 diagnostic=expected Android peer not found",
}
if allow_missing_peer and missing_peer:
return False
raise RuntimeError(diagnostic)
return True
@ -135,7 +139,12 @@ def main() -> int:
if args.mtp_adapter is None:
raise RuntimeError("MTP adapter is required")
pump = lambda: run_adapter(
args.mtp_adapter, "pull", args.expected_peer, args.transport_root, deadline, True
args.mtp_adapter,
"pull",
args.expected_peer,
args.transport_root,
deadline,
True,
)
# Android publishes its durable identity only while participating. A
# missing expected advertisement is therefore a bounded wait state;
@ -158,7 +167,9 @@ def main() -> int:
request_path = args.transport_root / "request-v1.json"
publish_json(request_path, request)
if args.mode == "mtp":
run_adapter(args.mtp_adapter, "push", args.expected_peer, args.transport_root, deadline)
run_adapter(
args.mtp_adapter, "push", args.expected_peer, args.transport_root, deadline
)
emit(
args.run_id,
"waiting_android_publication",
@ -174,7 +185,9 @@ def main() -> int:
ack_path = args.transport_root / "desktop-consumption-ack-v1.json"
publish_json(ack_path, ack)
if args.mode == "mtp":
run_adapter(args.mtp_adapter, "push", args.expected_peer, args.transport_root, deadline)
run_adapter(
args.mtp_adapter, "push", args.expected_peer, args.transport_root, deadline
)
if ack["result"] != "consumed":
raise RuntimeError("desktop rejected Android generation")
emit(
@ -205,8 +218,15 @@ def main() -> int:
}
publish_json(args.transport_root / "desktop-generation-v1.json", ref)
if args.mode == "mtp":
run_adapter(args.mtp_adapter, "push", args.expected_peer, args.transport_root, deadline)
emit(args.run_id, "published", outbound_generation_id=outgoing_id, manifest_sha256=digest)
run_adapter(
args.mtp_adapter, "push", args.expected_peer, args.transport_root, deadline
)
emit(
args.run_id,
"published",
outbound_generation_id=outgoing_id,
manifest_sha256=digest,
)
emit(args.run_id, "waiting_acknowledgement", outbound_generation_id=outgoing_id)
android_ack = args.transport_root / "android-consumption-ack-v1.json"
android_ack_value = wait_json(
@ -276,4 +296,8 @@ def main() -> int:
if __name__ == "__main__":
raise SystemExit(main())
try:
raise SystemExit(main())
except RuntimeError as error:
print(f"sync peer failed: {error}", file=sys.stderr)
raise SystemExit(1) from None

View file

@ -2,17 +2,25 @@
#define TRAINLOG_WEB_SERVER_H
#include <signal.h>
#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>
#include "trainlog/database.h"
/* Serialize the exact successful sync-admission document. Both identities are
* borrowed, validated sy_<uuid-v4> values. On failure output is empty. */
bool trainlog_web_sync_accepted_serialize(
const char *request_id, const char *run_id, char *output, size_t capacity, size_t *output_size);
/* Blocking, single-threaded local HTTP adapter. Port zero is reserved for
* controlled test harnesses and is rejected by the CLI. The database remains
* borrowed and process-owned; this infrastructure route never reads it. */
int trainlog_web_server_run(TrainlogDatabase *database, const char *database_path,
uint16_t port,
const volatile sig_atomic_t *stop_requested,
char *diagnostic, size_t diagnostic_capacity);
int trainlog_web_server_run(TrainlogDatabase *database,
const char *database_path,
uint16_t port,
const volatile sig_atomic_t *stop_requested,
char *diagnostic,
size_t diagnostic_capacity);
#endif

File diff suppressed because it is too large Load diff

33
tui/tests/bounded_path.h Normal file
View file

@ -0,0 +1,33 @@
#ifndef TRAINLOG_TEST_BOUNDED_PATH_H
#define TRAINLOG_TEST_BOUNDED_PATH_H
#include <stdbool.h>
#include <stddef.h>
#include <string.h>
/* CONTRACT: failure leaves output empty and no caller may perform filesystem
* work with a partial path. Lengths include the separator and trailing NUL. */
static bool
trainlog_test_join_path(char *output, size_t capacity, const char *parent, const char *leaf) {
size_t parent_length;
size_t leaf_length;
if (output == NULL || capacity == 0U || parent == NULL || leaf == NULL) {
return false;
}
output[0] = '\0';
parent_length = strlen(parent);
leaf_length = strlen(leaf);
if (leaf_length > SIZE_MAX - 2U || parent_length > SIZE_MAX - leaf_length - 2U ||
parent_length + leaf_length + 2U > capacity) {
return false;
}
(void)memcpy(output, parent, parent_length);
output[parent_length] = '/';
(void)memcpy(output + parent_length + 1U, leaf, leaf_length);
output[parent_length + leaf_length + 1U] = '\0';
return true;
}
#endif

View file

@ -1,10 +1,12 @@
#include <dirent.h>
#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <unistd.h>
#include "trainlog/generation_mtp.h"
#include "bounded_path.h"
typedef struct Node {
uint32_t id, parent;
@ -15,13 +17,41 @@ typedef struct Node {
static Node nodes[2048];
static size_t node_count;
static const char *remote;
static bool scan_failed;
static uint32_t stable_path_id(const char *path) {
const unsigned char *cursor = (const unsigned char *)path;
uint32_t value = UINT32_C(2166136261);
while (*cursor != '\0') {
value ^= (uint32_t)*cursor++;
value *= UINT32_C(16777619);
}
return value == 0U || value == UINT32_MAX ? value ^ UINT32_C(0x80000000) : value;
}
static uint32_t scan(const char *path, uint32_t parent) {
DIR *d;
struct dirent *e;
uint32_t self = (uint32_t)(node_count + 1U);
Node *n = &nodes[node_count++];
uint32_t self;
Node *n;
struct stat st;
lstat(path, &st);
if (lstat(path, &st) != 0) {
if (errno != ENOENT) {
scan_failed = true;
}
return 0U;
}
if (node_count >= sizeof(nodes) / sizeof(nodes[0])) {
scan_failed = true;
return 0U;
}
self = stable_path_id(path);
for (size_t index = 0U; index < node_count; index++) {
if (nodes[index].id == self && strcmp(nodes[index].path, path) != 0) {
scan_failed = true;
return 0U;
}
}
n = &nodes[node_count++];
n->id = self;
n->parent = parent;
n->folder = S_ISDIR(st.st_mode);
@ -33,17 +63,23 @@ static uint32_t scan(const char *path, uint32_t parent) {
while ((e = readdir(d))) {
if (strcmp(e->d_name, ".") && strcmp(e->d_name, "..")) {
char child[1024];
snprintf(child, sizeof(child), "%s/%s", path, e->d_name);
if (!trainlog_test_join_path(child, sizeof(child), path, e->d_name)) {
scan_failed = true;
break;
}
scan(child, self);
}
}
closedir(d);
} else if (n->folder && errno != ENOENT) {
scan_failed = true;
}
return self;
}
static void rebuild(void) {
static bool rebuild(void) {
node_count = 0U;
scan(remote, UINT32_MAX);
scan_failed = false;
return scan(remote, UINT32_MAX) != 0U && !scan_failed;
}
static Node *node(uint32_t id) {
size_t i;
@ -87,7 +123,9 @@ static TrainlogStatus children(unsigned int b,
(void)b;
(void)d;
(void)s;
rebuild();
if (!rebuild()) {
return TRAINLOG_STATUS_INVALID_ARGUMENT;
}
if (p == UINT32_MAX) {
p = nodes[0].id;
}
@ -121,12 +159,16 @@ static TrainlogStatus ensure(unsigned int b,
(void)b;
(void)d;
(void)s;
rebuild();
if (!rebuild()) {
return TRAINLOG_STATUS_INVALID_ARGUMENT;
}
parent = node(p);
if (!parent) {
return TRAINLOG_STATUS_NOT_FOUND;
}
snprintf(path, sizeof(path), "%s/%s", parent->path, name);
if (!trainlog_test_join_path(path, sizeof(path), parent->path, name)) {
return TRAINLOG_STATUS_INVALID_ARGUMENT;
}
if (mkdir(path, 0700) == 0) {
*created = true;
} else if (access(path, F_OK) == 0) {
@ -134,7 +176,9 @@ static TrainlogStatus ensure(unsigned int b,
} else {
return TRAINLOG_STATUS_SYSTEM_ERROR;
}
rebuild();
if (!rebuild()) {
return TRAINLOG_STATUS_INVALID_ARGUMENT;
}
size_t i;
for (i = 0; i < node_count; i++) {
if (strcmp(nodes[i].path, path) == 0) {
@ -162,7 +206,9 @@ static TrainlogStatus receive(unsigned int b, unsigned int d, uint32_t id, const
Node *n;
(void)b;
(void)d;
rebuild();
if (!rebuild()) {
return TRAINLOG_STATUS_INVALID_ARGUMENT;
}
n = node(id);
return n && copy(n->path, path) == 0 ? TRAINLOG_STATUS_OK : TRAINLOG_STATUS_SYSTEM_ERROR;
}
@ -178,16 +224,22 @@ static TrainlogStatus send_file(unsigned int b,
(void)b;
(void)d;
(void)s;
rebuild();
if (!rebuild()) {
return TRAINLOG_STATUS_INVALID_ARGUMENT;
}
parent = node(p);
if (!parent) {
return TRAINLOG_STATUS_NOT_FOUND;
}
snprintf(path, sizeof(path), "%s/%s", parent->path, name);
if (!trainlog_test_join_path(path, sizeof(path), parent->path, name)) {
return TRAINLOG_STATUS_INVALID_ARGUMENT;
}
if (copy(local, path) != 0) {
return TRAINLOG_STATUS_SYSTEM_ERROR;
}
rebuild();
if (!rebuild()) {
return TRAINLOG_STATUS_INVALID_ARGUMENT;
}
size_t i;
for (i = 0; i < node_count; i++) {
if (strcmp(nodes[i].path, path) == 0) {
@ -201,26 +253,38 @@ static TrainlogStatus remove_object(unsigned int b, unsigned int d, uint32_t id)
Node *n;
(void)b;
(void)d;
rebuild();
if (!rebuild()) {
return TRAINLOG_STATUS_INVALID_ARGUMENT;
}
n = node(id);
return n && unlink(n->path) == 0 ? TRAINLOG_STATUS_OK : TRAINLOG_STATUS_SYSTEM_ERROR;
}
static TrainlogStatus rename_object(unsigned int b, unsigned int d, uint32_t id, const char *name) {
Node *n;
char path[1024], *slash;
char parent[1024];
char path[1024];
char *slash;
(void)b;
(void)d;
rebuild();
if (!rebuild()) {
return TRAINLOG_STATUS_INVALID_ARGUMENT;
}
n = node(id);
if (!n) {
return TRAINLOG_STATUS_NOT_FOUND;
}
snprintf(path, sizeof(path), "%s", n->path);
slash = strrchr(path, '/');
if (strlen(n->path) >= sizeof(parent)) {
return TRAINLOG_STATUS_INVALID_ARGUMENT;
}
(void)memcpy(parent, n->path, strlen(n->path) + 1U);
slash = strrchr(parent, '/');
if (!slash) {
return TRAINLOG_STATUS_SYSTEM_ERROR;
}
snprintf(slash + 1, (size_t)(path + sizeof(path) - (slash + 1)), "%s", name);
*slash = '\0';
if (!trainlog_test_join_path(path, sizeof(path), parent, name)) {
return TRAINLOG_STATUS_INVALID_ARGUMENT;
}
return rename(n->path, path) == 0 ? TRAINLOG_STATUS_OK : TRAINLOG_STATUS_SYSTEM_ERROR;
}
static const TrainlogGenerationMtpIo io = {

View file

@ -4,6 +4,7 @@
#include <sys/stat.h>
#include <unistd.h>
#include "trainlog/generation_mtp.h"
#include "bounded_path.h"
#define CHECK(x) \
do { \
@ -239,21 +240,22 @@ static int test_pull_and_manifest_last(void) {
CHECK(trainlog_generation_mtp_pull(
&io, "peer_11111111-1111-4111-8111-111111111111", root, diag, sizeof(diag)) ==
TRAINLOG_STATUS_OK);
snprintf(path,
sizeof(path),
"%s/android-objects/generations/gen_11111111-1111-4111-8111-111111111111/payload.json",
root);
CHECK(trainlog_test_join_path(
path,
sizeof(path),
root,
"android-objects/generations/gen_11111111-1111-4111-8111-111111111111/payload.json"));
CHECK(access(path, R_OK) == 0);
snprintf(path, sizeof(path), "%s/out", root);
CHECK(trainlog_test_join_path(path, sizeof(path), root, "out"));
CHECK(mkdir(path, 0700) == 0);
char gen[512];
snprintf(gen, sizeof(gen), "%s/generation", path);
CHECK(trainlog_test_join_path(gen, sizeof(gen), path, "generation"));
CHECK(mkdir(gen, 0700) == 0);
char artifact[512];
snprintf(artifact, sizeof(artifact), "%s/artifact.json", gen);
CHECK(trainlog_test_join_path(artifact, sizeof(artifact), gen, "artifact.json"));
CHECK(write_file(artifact, "a") == 0);
char marker[512];
snprintf(marker, sizeof(marker), "%s/manifest.json", gen);
CHECK(trainlog_test_join_path(marker, sizeof(marker), gen, "manifest.json"));
CHECK(write_file(marker, "m") == 0);
CHECK(trainlog_generation_mtp_push(
&io, "peer_11111111-1111-4111-8111-111111111111", root, diag, sizeof(diag)) ==
@ -290,11 +292,36 @@ static int test_ambiguity_and_truncation(void) {
status = trainlog_generation_mtp_pull(
&io, "peer_11111111-1111-4111-8111-111111111111", root, diag, sizeof(diag));
CHECK(status != TRAINLOG_STATUS_OK);
snprintf(path, sizeof(path), "%s/bad.bin", root);
CHECK(trainlog_test_join_path(path, sizeof(path), root, "bad.bin"));
CHECK(access(path, F_OK) != 0);
return 0;
}
static int test_bounded_path_join(void) {
char exact[8];
char too_small[7] = "guard";
char multibyte[16];
char private_root[] = "/tmp/trainlog-path-bound-XXXXXX";
char sentinel[128];
char refused[8] = "guard";
CHECK(trainlog_test_join_path(exact, sizeof(exact), "abc", "def"));
CHECK(strcmp(exact, "abc/def") == 0);
CHECK(!trainlog_test_join_path(too_small, sizeof(too_small), "abc", "def"));
CHECK(too_small[0] == '\0');
CHECK(trainlog_test_join_path(multibyte, sizeof(multibyte), "rép", "é"));
CHECK(strcmp(multibyte, "rép/é") == 0);
CHECK(mkdtemp(private_root) != NULL);
CHECK(trainlog_test_join_path(sentinel, sizeof(sentinel), private_root, "sentinel"));
CHECK(write_file(sentinel, "outside") == 0);
CHECK(!trainlog_test_join_path(refused, sizeof(refused), private_root, "overflow"));
CHECK(refused[0] == '\0');
CHECK(access(sentinel, R_OK) == 0);
CHECK(unlink(sentinel) == 0);
CHECK(rmdir(private_root) == 0);
return 0;
}
int main(void) {
CHECK(test_bounded_path_join() == 0);
CHECK(test_pull_and_manifest_last() == 0);
CHECK(test_ambiguity_and_truncation() == 0);
return 0;

View file

@ -67,7 +67,7 @@ static bool run_test(void)
char path[] = "/tmp/trainlog-v16-XXXXXX";
int fd = mkstemp(path); sqlite3 *raw = NULL; TrainlogDatabase *db = NULL;
bool ok = false;
TrainlogPersistedExerciseDetail old[1] = {{0}};
TrainlogPersistedExerciseDetail old[1];
TrainlogSessionSummary summary;
size_t count = 0U; int version = 0;
TrainlogExercise changed;
@ -75,6 +75,7 @@ static bool run_test(void)
TrainlogSessionExerciseInput input = {0};
TrainlogSessionInput session = {0};
char diagnostic[512] = {0};
(void)memset(old, 0, sizeof(old));
CHECK(fd >= 0); CHECK(close(fd) == 0); fd = -1;
CHECK(sqlite3_open(path, &raw) == SQLITE_OK);
CHECK(sqlite3_exec(raw, FIXTURE, NULL, NULL, NULL) == SQLITE_OK);

View file

@ -149,6 +149,31 @@ static bool response_header(const char *response, const char *name, char *output
return true;
}
static bool test_sync_accepted_serialization(void) {
static const char REQUEST_ID[] = "sy_11111111-1111-4111-8111-111111111111";
static const char RUN_ID[] = "sy_22222222-2222-4222-8222-222222222222";
char output[256];
char exact[256];
char too_small[256];
size_t output_size;
CHECK(trainlog_web_sync_accepted_serialize(
REQUEST_ID, RUN_ID, output, sizeof(output), &output_size));
CHECK(output_size == strlen(output));
CHECK(strstr(output, "\"request_id\":\"sy_11111111-1111-4111-8111-111111111111\"") != NULL);
CHECK(strstr(output, "\"run_id\":\"sy_22222222-2222-4222-8222-222222222222\"") != NULL);
CHECK(strstr(output, "\"result\":\"running\"") != NULL);
CHECK(trainlog_web_sync_accepted_serialize(
REQUEST_ID, RUN_ID, exact, output_size + 1U, &output_size));
CHECK(strcmp(exact, output) == 0);
CHECK(!trainlog_web_sync_accepted_serialize(
REQUEST_ID, RUN_ID, too_small, output_size, &output_size));
CHECK(too_small[0] == '\0');
CHECK(!trainlog_web_sync_accepted_serialize(
"sy_invalid", RUN_ID, output, sizeof(output), &output_size));
return true;
}
static bool test_http_contract(TrainlogDatabase *database) {
char response[32768];
char large_request[12000];
@ -476,8 +501,8 @@ int main(void) {
sizeof(diagnostic)) != 0 &&
strstr(diagnostic, "frontend") != NULL;
} else {
passed = test_http_contract(database) && test_port_in_use(database) &&
test_dashboard_core_error_translation();
passed = test_sync_accepted_serialization() && test_http_contract(database) &&
test_port_in_use(database) && test_dashboard_core_error_translation();
}
trainlog_database_close(database);
return passed ? 0 : 1;