Compare commits
No commits in common. "2a68e5a5052f0d9eef1bfdfce3fd1cccff86a075" and "f27ef5af5f8cb23b96ad41d14ed5e309bf7dd737" have entirely different histories.
2a68e5a505
...
f27ef5af5f
18 changed files with 68 additions and 418 deletions
|
|
@ -4,15 +4,6 @@
|
||||||
|
|
||||||
Development opened after the stable v0.1.2 release.
|
Development opened after the stable v0.1.2 release.
|
||||||
|
|
||||||
- Routed Android foreground and daemon-triggered daily synchronization through
|
|
||||||
the existing full-generation orchestrator whenever its trusted local
|
|
||||||
configuration is present. The standalone mobile V3 importer remains strict
|
|
||||||
and still rejects causally protected live replay; complete generations apply
|
|
||||||
tombstones before filtering dominated snapshot facts. No schema, exchange
|
|
||||||
format, causal rule, or synchronization protocol changed. Android backup
|
|
||||||
manifests now obtain `product_version` from installed package metadata
|
|
||||||
instead of retaining the historical `0.1.2` literal.
|
|
||||||
|
|
||||||
- Unified the Android capture interface around compact dark cards, action
|
- Unified the Android capture interface around compact dark cards, action
|
||||||
tiles, restrained-radius button variants, wrapping selection grids, and
|
tiles, restrained-radius button variants, wrapping selection grids, and
|
||||||
two-column performed-set entry. Home now focuses on session capture, recent
|
two-column performed-set entry. Home now focuses on session capture, recent
|
||||||
|
|
|
||||||
|
|
@ -26,13 +26,7 @@ internal data class VerifiedAndroidBackup(
|
||||||
)
|
)
|
||||||
|
|
||||||
/** Complete same-installation backup container; plaintext is explicit by design. */
|
/** Complete same-installation backup container; plaintext is explicit by design. */
|
||||||
internal class AndroidBackupService(
|
internal class AndroidBackupService(private val context: Context) {
|
||||||
private val context: Context,
|
|
||||||
private val productVersion: String =
|
|
||||||
checkNotNull(
|
|
||||||
context.packageManager.getPackageInfo(context.packageName, 0).versionName,
|
|
||||||
),
|
|
||||||
) {
|
|
||||||
companion object {
|
companion object {
|
||||||
const val FORMAT = "trainlog-android-backup"
|
const val FORMAT = "trainlog-android-backup"
|
||||||
const val VERSION = 1
|
const val VERSION = 1
|
||||||
|
|
@ -146,7 +140,7 @@ internal class AndroidBackupService(
|
||||||
.put("version", VERSION)
|
.put("version", VERSION)
|
||||||
.put("package", context.packageName)
|
.put("package", context.packageName)
|
||||||
.put("schema", schema)
|
.put("schema", schema)
|
||||||
.put("product_version", productVersion)
|
.put("product_version", "0.1.2")
|
||||||
.put("plaintext", true)
|
.put("plaintext", true)
|
||||||
.put("created_at", OffsetDateTime.now().toString())
|
.put("created_at", OffsetDateTime.now().toString())
|
||||||
.put(
|
.put(
|
||||||
|
|
|
||||||
|
|
@ -50,37 +50,39 @@ internal suspend fun publishBundleAndRequest(
|
||||||
exporter: SyncExporter,
|
exporter: SyncExporter,
|
||||||
requestOutbox: SyncRequestOutbox,
|
requestOutbox: SyncRequestOutbox,
|
||||||
): SyncRequestResult = withContext(Dispatchers.IO) {
|
): SyncRequestResult = withContext(Dispatchers.IO) {
|
||||||
val opened = exporter.openStorageSnapshot()
|
val optIn = java.io.File(canonicalExchangeDirectory(), "trainlog-sync-generation-opt-in-v1.json")
|
||||||
if (opened is com.labfytools.trainlog.data.DirectExchangeSnapshotResult.Error) {
|
if (optIn.isFile) {
|
||||||
return@withContext SyncRequestResult.Error("prepare:${opened.message}")
|
val enabled = try {
|
||||||
|
val value = org.json.JSONObject(optIn.readText())
|
||||||
|
value.keys().asSequence().toSet() == setOf("format", "version", "enabled") &&
|
||||||
|
value.optString("format") == "trainlog-sync-generation-opt-in" &&
|
||||||
|
value.opt("version") is Int && value.getInt("version") == 1 && value.opt("enabled") == true
|
||||||
|
} catch (_: Exception) { false }
|
||||||
|
if (!enabled) return@withContext SyncRequestResult.Error("Invalid generation synchronization opt-in.")
|
||||||
|
val opened = exporter.openStorageSnapshot()
|
||||||
|
if (opened is com.labfytools.trainlog.data.DirectExchangeSnapshotResult.Error) {
|
||||||
|
return@withContext SyncRequestResult.Error("prepare:${opened.message}")
|
||||||
|
}
|
||||||
|
val snapshot = (opened as com.labfytools.trainlog.data.DirectExchangeSnapshotResult.Ready).snapshot
|
||||||
|
return@withContext when (
|
||||||
|
val result = SyncGenerationForegroundCoordinator(repository).run(
|
||||||
|
canonicalExchangeDirectory(),
|
||||||
|
afterPeerPublication = {
|
||||||
|
when (val request = requestOutbox.requestSync(snapshot)) {
|
||||||
|
is SyncRequestResult.Requested -> Unit
|
||||||
|
SyncRequestResult.Unsupported ->
|
||||||
|
throw IllegalStateException("Generation synchronization is unsupported.")
|
||||||
|
is SyncRequestResult.Error -> throw IllegalStateException(request.message)
|
||||||
|
is SyncRequestResult.Completed ->
|
||||||
|
throw IllegalStateException("Unexpected completed request signal.")
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
is ForegroundGenerationResult.Completed -> SyncRequestResult.Completed(result.runId)
|
||||||
|
is ForegroundGenerationResult.Error -> SyncRequestResult.Error(result.message)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
val snapshot = (opened as com.labfytools.trainlog.data.DirectExchangeSnapshotResult.Ready).snapshot
|
|
||||||
return@withContext when (
|
|
||||||
val result = SyncGenerationForegroundCoordinator(repository).run(
|
|
||||||
canonicalExchangeDirectory(),
|
|
||||||
afterPeerPublication = {
|
|
||||||
when (val request = requestOutbox.requestSync(snapshot)) {
|
|
||||||
is SyncRequestResult.Requested -> Unit
|
|
||||||
SyncRequestResult.Unsupported ->
|
|
||||||
throw IllegalStateException("Generation synchronization is unsupported.")
|
|
||||||
is SyncRequestResult.Error -> throw IllegalStateException(request.message)
|
|
||||||
is SyncRequestResult.Completed ->
|
|
||||||
throw IllegalStateException("Unexpected completed request signal.")
|
|
||||||
}
|
|
||||||
},
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
is ForegroundGenerationResult.Completed -> SyncRequestResult.Completed(result.runId)
|
|
||||||
is ForegroundGenerationResult.Error -> SyncRequestResult.Error(result.message)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Compatibility tooling keeps the standalone V3 publisher strict and
|
|
||||||
* available, but production Android triggers no longer select this path. */
|
|
||||||
internal suspend fun publishLegacyBundleAndRequest(
|
|
||||||
exporter: SyncExporter,
|
|
||||||
requestOutbox: SyncRequestOutbox,
|
|
||||||
): SyncRequestResult = withContext(Dispatchers.IO) {
|
|
||||||
logDirectExchange("SYNC_BUNDLE", "coordinator.export.begin")
|
logDirectExchange("SYNC_BUNDLE", "coordinator.export.begin")
|
||||||
when (val opened = exporter.openStorageSnapshot()) {
|
when (val opened = exporter.openStorageSnapshot()) {
|
||||||
is com.labfytools.trainlog.data.DirectExchangeSnapshotResult.Error ->
|
is com.labfytools.trainlog.data.DirectExchangeSnapshotResult.Error ->
|
||||||
|
|
@ -88,7 +90,9 @@ internal suspend fun publishLegacyBundleAndRequest(
|
||||||
is com.labfytools.trainlog.data.DirectExchangeSnapshotResult.Ready ->
|
is com.labfytools.trainlog.data.DirectExchangeSnapshotResult.Ready ->
|
||||||
when (val publication = exporter.exportMobileBundle(opened.snapshot)) {
|
when (val publication = exporter.exportMobileBundle(opened.snapshot)) {
|
||||||
SyncExportResult.Unsupported -> SyncRequestResult.Unsupported
|
SyncExportResult.Unsupported -> SyncRequestResult.Unsupported
|
||||||
is SyncExportResult.Error -> SyncRequestResult.Error("prepare:${publication.message}")
|
is SyncExportResult.Error -> SyncRequestResult.Error(
|
||||||
|
"prepare:${publication.message}",
|
||||||
|
)
|
||||||
is SyncExportResult.Exported -> {
|
is SyncExportResult.Exported -> {
|
||||||
logDirectExchange("SYNC_BUNDLE", "coordinator.export.success")
|
logDirectExchange("SYNC_BUNDLE", "coordinator.export.success")
|
||||||
logDirectExchange("trainlog-sync-request-v1.json", "coordinator.request.begin")
|
logDirectExchange("trainlog-sync-request-v1.json", "coordinator.request.begin")
|
||||||
|
|
|
||||||
|
|
@ -12,8 +12,6 @@ import com.labfytools.trainlog.model.TrackingMode
|
||||||
import java.io.ByteArrayInputStream
|
import java.io.ByteArrayInputStream
|
||||||
import java.io.ByteArrayOutputStream
|
import java.io.ByteArrayOutputStream
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
import java.util.zip.ZipInputStream
|
|
||||||
import org.json.JSONObject
|
|
||||||
import org.junit.Assert.assertEquals
|
import org.junit.Assert.assertEquals
|
||||||
import org.junit.Assert.assertTrue
|
import org.junit.Assert.assertTrue
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
|
|
@ -26,30 +24,6 @@ import org.robolectric.annotation.Config
|
||||||
class AndroidBackupServiceTest {
|
class AndroidBackupServiceTest {
|
||||||
private val context: Context = ApplicationProvider.getApplicationContext()
|
private val context: Context = ApplicationProvider.getApplicationContext()
|
||||||
|
|
||||||
@Test
|
|
||||||
fun manifestUsesInjectedCanonicalProductVersion() {
|
|
||||||
val name = "backup-version-${UUID.randomUUID()}.db"
|
|
||||||
val repository = TrainlogRepository(context, name)
|
|
||||||
try {
|
|
||||||
val output = ByteArrayOutputStream()
|
|
||||||
val service = AndroidBackupService(context, "0.1.3-test")
|
|
||||||
assertTrue(service.create(repository, output) is AndroidBackupResult.Success)
|
|
||||||
var manifest: JSONObject? = null
|
|
||||||
ZipInputStream(ByteArrayInputStream(output.toByteArray())).use { zip ->
|
|
||||||
while (true) {
|
|
||||||
val entry = zip.nextEntry ?: break
|
|
||||||
if (entry.name == "manifest.json") {
|
|
||||||
manifest = JSONObject(zip.readBytes().toString(Charsets.UTF_8))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
assertEquals("0.1.3-test", checkNotNull(manifest).getString("product_version"))
|
|
||||||
} finally {
|
|
||||||
repository.close()
|
|
||||||
context.deleteDatabase(name)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun completeBackupRestoresDraftPeerLedgersAndPreferences() {
|
fun completeBackupRestoresDraftPeerLedgersAndPreferences() {
|
||||||
val name = "backup-${UUID.randomUUID()}.db"
|
val name = "backup-${UUID.randomUUID()}.db"
|
||||||
|
|
|
||||||
|
|
@ -100,7 +100,7 @@ class SyncBundlePublicationTest {
|
||||||
assertTrue(feedback is SaveFeedbackResult.Saved)
|
assertTrue(feedback is SaveFeedbackResult.Saved)
|
||||||
|
|
||||||
val writesBeforeFirstClick = directory.writeCounts.toMap()
|
val writesBeforeFirstClick = directory.writeCounts.toMap()
|
||||||
assertTrue(publishLegacyBundleAndRequest(exporter, outbox) is SyncRequestResult.Requested)
|
assertTrue(publishBundleAndRequest(repository, exporter, outbox) is SyncRequestResult.Requested)
|
||||||
assertEquals(2, directory.enumerationCount)
|
assertEquals(2, directory.enumerationCount)
|
||||||
val required = listOf(
|
val required = listOf(
|
||||||
"trainlog-mobile-export-v3.json",
|
"trainlog-mobile-export-v3.json",
|
||||||
|
|
@ -138,7 +138,7 @@ class SyncBundlePublicationTest {
|
||||||
ShadowLog.clear()
|
ShadowLog.clear()
|
||||||
val bodyCreatesBefore = directory.createCount("trainlog-exercise-body-zones-v1.json")
|
val bodyCreatesBefore = directory.createCount("trainlog-exercise-body-zones-v1.json")
|
||||||
val writesBeforeSecondClick = directory.writeCounts.toMap()
|
val writesBeforeSecondClick = directory.writeCounts.toMap()
|
||||||
assertTrue(publishLegacyBundleAndRequest(exporter, outbox) is SyncRequestResult.Requested)
|
assertTrue(publishBundleAndRequest(repository, exporter, outbox) is SyncRequestResult.Requested)
|
||||||
assertEquals(3, directory.enumerationCount)
|
assertEquals(3, directory.enumerationCount)
|
||||||
val phases = ShadowLog.getLogsForTag(DIRECT_EXCHANGE_LOG_TAG).map { it.msg }
|
val phases = ShadowLog.getLogsForTag(DIRECT_EXCHANGE_LOG_TAG).map { it.msg }
|
||||||
assertEquals(1, phases.count { it.startsWith("SYNC_BUNDLE coordinator.export.begin") })
|
assertEquals(1, phases.count { it.startsWith("SYNC_BUNDLE coordinator.export.begin") })
|
||||||
|
|
|
||||||
|
|
@ -1056,17 +1056,10 @@ coordinator waits for correlated objects, tolerates stale objects left by
|
||||||
interrupted runs, strictly revalidates desktop-retained historical ACKs, and
|
interrupted runs, strictly revalidates desktop-retained historical ACKs, and
|
||||||
shows completion only after both durable peer acknowledgements.
|
shows completion only after both durable peer acknowledgements.
|
||||||
|
|
||||||
Each explicit foreground synchronization attempt publishes the Android peer
|
Each explicit foreground generation attempt publishes the Android peer before
|
||||||
and enters the full-generation coordinator before emitting one fresh request
|
emitting one fresh legacy request signal for `trainlog-syncd`. If transport is
|
||||||
signal for `trainlog-syncd`. The signal is only admission/correlation; a
|
|
||||||
configured daemon routes it to the existing desktop orchestrator rather than
|
|
||||||
the standalone V3 importer. If transport is
|
|
||||||
interrupted after generation publication, a later attempt resumes the exact
|
interrupted after generation publication, a later attempt resumes the exact
|
||||||
stored generation instead of recapturing mutable data. An acknowledged Android
|
stored generation instead of recapturing mutable data. An acknowledged Android
|
||||||
generation remains resumable only until the desktop generation for that run is
|
generation remains resumable only until the desktop generation for that run is
|
||||||
durably consumed; a fully completed run is then excluded from foreground
|
durably consumed; a fully completed run is then excluded from foreground
|
||||||
selection. This preserves late-ACK recovery without replaying completed runs.
|
selection. This preserves late-ACK recovery without replaying completed runs.
|
||||||
|
|
||||||
Backup manifests retain format version 1 and the current Android schema. Their
|
|
||||||
`product_version` is read from installed package metadata, so development
|
|
||||||
builds record `0.1.3` without a second hard-coded product-version source.
|
|
||||||
|
|
|
||||||
|
|
@ -456,11 +456,10 @@ manifest is published after its member objects. The helper runs inside the
|
||||||
orchestrator-owned process group and cannot be selected or parameterized by an
|
orchestrator-owned process group and cannot be selected or parameterized by an
|
||||||
HTTP request.
|
HTTP request.
|
||||||
|
|
||||||
It polls for a new Android request. With trusted full-generation configuration
|
It polls for a new Android request and invokes the same shared C synchronization
|
||||||
it admits that request to the existing application orchestrator and peer
|
engine used by the TUI.
|
||||||
worker; without that configuration it retains the shared C/V3 compatibility
|
|
||||||
engine used by the TUI. It does not implement a second synchronization
|
It does not implement a second synchronization algorithm.
|
||||||
algorithm.
|
|
||||||
|
|
||||||
## 3. Exercise model
|
## 3. Exercise model
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -152,20 +152,15 @@ Android inbox/outbox still select V3 and do not move the draft artifact.
|
||||||
boundaries, not active storage choices.
|
boundaries, not active storage choices.
|
||||||
- Desktop synchronization uses direct libudev/libmtp discovery and object
|
- Desktop synchronization uses direct libudev/libmtp discovery and object
|
||||||
access. No GVFS/FUSE mount is required.
|
access. No GVFS/FUSE mount is required.
|
||||||
- `trainlog_sync_run()` remains the strict legacy V3 engine shared with the
|
- `trainlog_sync_run()` is shared by the TUI and `trainlog-syncd`.
|
||||||
TUI and compatibility tooling. When the trusted full-generation
|
|
||||||
configuration is present, `trainlog-syncd` admits the Android request into
|
|
||||||
the existing `sync_orchestrator.py`/`sync_peer_worker.py` conversation;
|
|
||||||
configuration absence retains the legacy fallback.
|
|
||||||
- The desktop imports Android snapshots, publishes catalog/profile/feedback/AI
|
- The desktop imports Android snapshots, publishes catalog/profile/feedback/AI
|
||||||
companions, and returns a receipt. Stable IDs, artifact idempotence and the
|
companions, and returns a receipt. Stable IDs, artifact idempotence and the
|
||||||
few domain-specific explicit states make defined replay paths idempotent.
|
few domain-specific explicit states make defined replay paths idempotent.
|
||||||
Explicit staged generation services now provide coherent V4/companion
|
Explicit staged generation services now provide coherent V4/companion
|
||||||
capture, immutable manifests, whole-generation SQLite consumption and
|
capture, immutable manifests, whole-generation SQLite consumption and
|
||||||
durable peer ACKs. The Android foreground trigger now publishes a complete
|
durable peer ACKs. A trusted opt-in configuration may select the bounded
|
||||||
generation by default. A trusted desktop configuration selects the bounded
|
|
||||||
production generation MTP adapter; absent configuration leaves V3 as the
|
production generation MTP adapter; absent configuration leaves V3 as the
|
||||||
compatibility fallback.
|
default.
|
||||||
- SQLite database files are never synchronized.
|
- SQLite database files are never synchronized.
|
||||||
- The desktop AI flow uses external `rclone` for Drive inbox/archive and
|
- The desktop AI flow uses external `rclone` for Drive inbox/archive and
|
||||||
read-only history export. Android owns no Drive credentials.
|
read-only history export. Android owns no Drive credentials.
|
||||||
|
|
|
||||||
|
|
@ -135,11 +135,9 @@ Synchronization artifacts are separate from the frozen Trainlog session JSON
|
||||||
v1 format.
|
v1 format.
|
||||||
|
|
||||||
V4 history and execution-draft V1 are staged codecs with explicit repository
|
V4 history and execution-draft V1 are staged codecs with explicit repository
|
||||||
and command-line entry points. They remain deliberately absent from
|
and command-line entry points. They are deliberately absent from
|
||||||
`trainlog_sync_run()`: configured Android/daemon requests now route around that
|
`trainlog_sync_run()`, request/receipt publication and the Android automatic
|
||||||
strict legacy V3 engine into the existing full-generation orchestrator. The
|
inbox/outbox. Deployment must stop the old user worker, replace the installed
|
||||||
request artifact remains only a correlated admission signal. Deployment must
|
|
||||||
stop the old user worker, replace the installed
|
|
||||||
binary/scripts as one unit, migrate only through the new binary, and restart
|
binary/scripts as one unit, migrate only through the new binary, and restart
|
||||||
only after compatibility checks; this ticket performs none of those steps.
|
only after compatibility checks; this ticket performs none of those steps.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1512,18 +1512,6 @@ the `trainlog-syncd` launcher and daemon source, and the generation worker.
|
||||||
This prevents a user unit from silently executing a helper from a different
|
This prevents a user unit from silently executing a helper from a different
|
||||||
checkout than the installed desktop candidate.
|
checkout than the installed desktop candidate.
|
||||||
|
|
||||||
`syncd_routing` proves that both Android-labelled and daemon-labelled
|
|
||||||
admissions invoke the packaged `sync_orchestrator.py` full-generation owner
|
|
||||||
when trusted configuration is present, and never invoke the legacy
|
|
||||||
`trainlog-sync-once` command in that mode. The generation-MTP boundary test
|
|
||||||
proves that polling copies the Android request signal without acknowledging or
|
|
||||||
deleting it. Session-exchange characterization keeps standalone V3 rejection
|
|
||||||
strict while its complete-envelope case filters a tombstone-dominated live
|
|
||||||
fact; the causal Android/Desktop round-trip and generation replay/ACK tests
|
|
||||||
cover restart, idempotence and non-resurrection. `AndroidBackupServiceTest`
|
|
||||||
injects a product version and verifies that exact value in the ZIP manifest,
|
|
||||||
preventing a return to a hard-coded release string.
|
|
||||||
|
|
||||||
The isolated JDK 17 harness reports **220 Android tests: 215 passed, five
|
The isolated JDK 17 harness reports **220 Android tests: 215 passed, five
|
||||||
skipped, zero failures/errors**. The skips are optional, externally supplied
|
skipped, zero failures/errors**. The skips are optional, externally supplied
|
||||||
copy gates: `BridgeBackupMigrationTest.verifiedSchemaSeventeenBridgeBackupMigratesThroughCurrentOwner`,
|
copy gates: `BridgeBackupMigrationTest.verifiedSchemaSeventeenBridgeBackupMigratesThroughCurrentOwner`,
|
||||||
|
|
|
||||||
|
|
@ -75,10 +75,6 @@ class DeploymentToolsTest(unittest.TestCase):
|
||||||
self.assertTrue((output / "bin/trainlog-sync-once").is_file())
|
self.assertTrue((output / "bin/trainlog-sync-once").is_file())
|
||||||
self.assertTrue((output / "bin/trainlog-syncd").is_file())
|
self.assertTrue((output / "bin/trainlog-syncd").is_file())
|
||||||
self.assertTrue((output / "libexec/trainlog-sync-once").is_file())
|
self.assertTrue((output / "libexec/trainlog-sync-once").is_file())
|
||||||
self.assertIn(
|
|
||||||
'TRAINLOG_SYNC_MTP_ADAPTER="$root/libexec/trainlog-generation-mtp-adapter"',
|
|
||||||
(output / "bin/trainlog-syncd").read_text(),
|
|
||||||
)
|
|
||||||
stable_bin = Path(directory) / "stable-bin"
|
stable_bin = Path(directory) / "stable-bin"
|
||||||
stable_bin.mkdir()
|
stable_bin.mkdir()
|
||||||
(stable_bin / "trainlog").symlink_to(output / "bin/trainlog")
|
(stable_bin / "trainlog").symlink_to(output / "bin/trainlog")
|
||||||
|
|
|
||||||
|
|
@ -1,88 +0,0 @@
|
||||||
#!/usr/bin/env python3
|
|
||||||
"""Regression coverage for Android/daemon full-generation admission."""
|
|
||||||
|
|
||||||
import argparse
|
|
||||||
import importlib.util
|
|
||||||
import json
|
|
||||||
import subprocess
|
|
||||||
import tempfile
|
|
||||||
import unittest
|
|
||||||
from pathlib import Path
|
|
||||||
from unittest import mock
|
|
||||||
|
|
||||||
ROOT = Path(__file__).resolve().parents[1]
|
|
||||||
SPEC = importlib.util.spec_from_file_location("trainlog_syncd", ROOT / "tools/trainlog_syncd.py")
|
|
||||||
syncd = importlib.util.module_from_spec(SPEC)
|
|
||||||
assert SPEC.loader is not None
|
|
||||||
SPEC.loader.exec_module(syncd)
|
|
||||||
|
|
||||||
|
|
||||||
class SyncdRoutingTest(unittest.TestCase):
|
|
||||||
def test_android_and_daemon_triggers_route_to_existing_orchestrator(self):
|
|
||||||
for trigger in ("android", "daemon"):
|
|
||||||
with self.subTest(trigger=trigger), tempfile.TemporaryDirectory() as directory:
|
|
||||||
root = Path(directory)
|
|
||||||
transport = root / "transport"
|
|
||||||
transport.mkdir()
|
|
||||||
config = root / "config.json"
|
|
||||||
config.write_text(
|
|
||||||
json.dumps(
|
|
||||||
{
|
|
||||||
"format": "trainlog-sync-orchestrator-config",
|
|
||||||
"version": 1,
|
|
||||||
"enabled": True,
|
|
||||||
"mode": "mtp",
|
|
||||||
"expected_peer_id": "peer_11111111-1111-4111-8111-111111111111",
|
|
||||||
"transport_root": str(transport),
|
|
||||||
"owned_root": str(root / "owned"),
|
|
||||||
"timeout_seconds": 30,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
)
|
|
||||||
args = argparse.Namespace(
|
|
||||||
mtp_adapter=root / "adapter",
|
|
||||||
state=root / "trainlog.db.sync-run.json",
|
|
||||||
trigger=trigger,
|
|
||||||
orchestrator=root / "sync_orchestrator.py",
|
|
||||||
database=root / "trainlog.db",
|
|
||||||
)
|
|
||||||
commands = []
|
|
||||||
|
|
||||||
def execute(command, **_kwargs):
|
|
||||||
commands.append(command)
|
|
||||||
if command[0] == str(args.mtp_adapter):
|
|
||||||
(transport / syncd.REQUEST_NAME).write_text(
|
|
||||||
json.dumps(
|
|
||||||
{
|
|
||||||
"format": "trainlog-sync-request",
|
|
||||||
"version": 1,
|
|
||||||
"request_id": "sr_22222222-2222-4222-8222-222222222222",
|
|
||||||
"requested_at": "2026-09-20T12:00:00Z",
|
|
||||||
}
|
|
||||||
)
|
|
||||||
)
|
|
||||||
return subprocess.CompletedProcess(command, 0, "PASS\n", "")
|
|
||||||
|
|
||||||
with mock.patch.object(syncd.subprocess, "run", side_effect=execute), mock.patch.object(
|
|
||||||
syncd, "append_log"
|
|
||||||
):
|
|
||||||
self.assertEqual(0, syncd.run_full_generation(args, config))
|
|
||||||
self.assertEqual(str(args.mtp_adapter), commands[0][0])
|
|
||||||
self.assertEqual(str(args.orchestrator), commands[1][1])
|
|
||||||
self.assertNotIn("trainlog-sync-once", " ".join(commands[1]))
|
|
||||||
state = json.loads(args.state.read_text())
|
|
||||||
self.assertEqual(trigger, state["trigger"])
|
|
||||||
self.assertEqual("full_generation_v1", state["effective_mode"])
|
|
||||||
|
|
||||||
def test_android_production_trigger_no_longer_uses_opt_in_or_legacy_export(self):
|
|
||||||
source = (ROOT / "android/app/src/main/java/com/labfytools/trainlog/ui/SyncScreen.kt").read_text()
|
|
||||||
production = source.split("internal suspend fun publishBundleAndRequest", 1)[1].split(
|
|
||||||
"internal suspend fun publishLegacyBundleAndRequest", 1
|
|
||||||
)[0]
|
|
||||||
self.assertIn("SyncGenerationForegroundCoordinator(repository).run", production)
|
|
||||||
self.assertNotIn("generation-opt-in", production)
|
|
||||||
self.assertNotIn("exportMobileBundle", production)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
unittest.main()
|
|
||||||
|
|
@ -9,7 +9,6 @@ ROOT="$(
|
||||||
)"
|
)"
|
||||||
|
|
||||||
SYNC_ONCE="$ROOT/build/tui/trainlog-sync-once"
|
SYNC_ONCE="$ROOT/build/tui/trainlog-sync-once"
|
||||||
MTP_ADAPTER="$ROOT/build/tui/trainlog-generation-mtp-adapter"
|
|
||||||
DAEMON="$ROOT/tools/trainlog_syncd.py"
|
DAEMON="$ROOT/tools/trainlog_syncd.py"
|
||||||
|
|
||||||
if [[ ! -x "$SYNC_ONCE" ]]; then
|
if [[ ! -x "$SYNC_ONCE" ]]; then
|
||||||
|
|
@ -18,12 +17,6 @@ if [[ ! -x "$SYNC_ONCE" ]]; then
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [[ ! -x "$MTP_ADAPTER" ]]; then
|
|
||||||
echo "missing executable: $MTP_ADAPTER" >&2
|
|
||||||
echo "run meson compile -C build first" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
mkdir -p \
|
mkdir -p \
|
||||||
"$HOME/.local/bin" \
|
"$HOME/.local/bin" \
|
||||||
"$HOME/.config/systemd/user"
|
"$HOME/.config/systemd/user"
|
||||||
|
|
@ -44,7 +37,7 @@ Description=Trainlog Android MTP synchronization agent
|
||||||
|
|
||||||
[Service]
|
[Service]
|
||||||
Type=simple
|
Type=simple
|
||||||
ExecStart=/usr/bin/python3 "$DAEMON" --sync-once "$SYNC_ONCE" --mtp-adapter "$MTP_ADAPTER" --interval 3
|
ExecStart=/usr/bin/python3 "$DAEMON" --sync-once "$SYNC_ONCE" --interval 3
|
||||||
Restart=on-failure
|
Restart=on-failure
|
||||||
RestartSec=2
|
RestartSec=2
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -94,7 +94,7 @@ def main() -> int:
|
||||||
)
|
)
|
||||||
syncd_launcher = args.output / "bin/trainlog-syncd"
|
syncd_launcher = args.output / "bin/trainlog-syncd"
|
||||||
syncd_launcher.write_text(
|
syncd_launcher.write_text(
|
||||||
'#!/bin/sh\nset -eu\nscript=$(readlink -f -- "$0")\nroot=$(CDPATH= cd -- "$(dirname -- "$script")/.." && pwd)\nexport TRAINLOG_SYNC_MTP_ADAPTER="$root/libexec/trainlog-generation-mtp-adapter"\nexec python3 "$root/tools/trainlog_syncd.py" --sync-once "$root/bin/trainlog-sync-once" "$@"\n'
|
'#!/bin/sh\nset -eu\nscript=$(readlink -f -- "$0")\nroot=$(CDPATH= cd -- "$(dirname -- "$script")/.." && pwd)\nexec python3 "$root/tools/trainlog_syncd.py" --sync-once "$root/bin/trainlog-sync-once" "$@"\n'
|
||||||
)
|
)
|
||||||
files = sorted(path for path in args.output.rglob("*") if path.is_file())
|
files = sorted(path for path in args.output.rglob("*") if path.is_file())
|
||||||
inventory = {
|
inventory = {
|
||||||
|
|
|
||||||
|
|
@ -4,22 +4,13 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
import json
|
|
||||||
import os
|
|
||||||
import re
|
|
||||||
import signal
|
import signal
|
||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
|
||||||
import tempfile
|
|
||||||
import time
|
import time
|
||||||
import uuid
|
|
||||||
from datetime import datetime, timezone
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
STOP = False
|
STOP = False
|
||||||
REQUEST_NAME = "trainlog-sync-request-v1.json"
|
|
||||||
REQUEST_ID = re.compile(r"sr_[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}")
|
|
||||||
|
|
||||||
|
|
||||||
def request_stop(
|
def request_stop(
|
||||||
|
|
@ -55,126 +46,6 @@ def append_log(
|
||||||
handle.write("\n")
|
handle.write("\n")
|
||||||
|
|
||||||
|
|
||||||
def default_database() -> Path:
|
|
||||||
data_home = Path(os.environ.get("XDG_DATA_HOME", Path.home() / ".local/share"))
|
|
||||||
return data_home / "trainlog/trainlog.db"
|
|
||||||
|
|
||||||
|
|
||||||
def atomic_json(path: Path, value: dict) -> None:
|
|
||||||
raw = (json.dumps(value, sort_keys=True, separators=(",", ":")) + "\n").encode()
|
|
||||||
path.parent.mkdir(parents=True, exist_ok=True, mode=0o700)
|
|
||||||
descriptor, temporary = tempfile.mkstemp(prefix=".sync-run-", dir=path.parent)
|
|
||||||
try:
|
|
||||||
os.fchmod(descriptor, 0o600)
|
|
||||||
with os.fdopen(descriptor, "wb") as stream:
|
|
||||||
stream.write(raw)
|
|
||||||
stream.flush()
|
|
||||||
os.fsync(stream.fileno())
|
|
||||||
os.replace(temporary, path)
|
|
||||||
finally:
|
|
||||||
try:
|
|
||||||
os.unlink(temporary)
|
|
||||||
except FileNotFoundError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
def load_generation_config(path: Path) -> dict:
|
|
||||||
value = json.loads(path.read_text(encoding="utf-8"))
|
|
||||||
if (
|
|
||||||
not isinstance(value, dict)
|
|
||||||
or value.get("format") != "trainlog-sync-orchestrator-config"
|
|
||||||
or value.get("enabled") is not True
|
|
||||||
or value.get("mode") not in ("mtp", "auto")
|
|
||||||
):
|
|
||||||
raise RuntimeError("trusted full-generation MTP configuration is invalid")
|
|
||||||
return value
|
|
||||||
|
|
||||||
|
|
||||||
def request_id(path: Path) -> str | None:
|
|
||||||
try:
|
|
||||||
value = json.loads(path.read_text(encoding="utf-8"))
|
|
||||||
except (OSError, UnicodeError, json.JSONDecodeError):
|
|
||||||
return None
|
|
||||||
if not isinstance(value, dict):
|
|
||||||
return None
|
|
||||||
candidate = value.get("request_id")
|
|
||||||
if (
|
|
||||||
set(value) == {"format", "version", "request_id", "requested_at"}
|
|
||||||
and value.get("format") == "trainlog-sync-request"
|
|
||||||
and type(value.get("version")) is int
|
|
||||||
and value.get("version") == 1
|
|
||||||
and isinstance(candidate, str)
|
|
||||||
and REQUEST_ID.fullmatch(candidate)
|
|
||||||
):
|
|
||||||
return candidate
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
def run_full_generation(args: argparse.Namespace, config_path: Path) -> int:
|
|
||||||
config = load_generation_config(config_path)
|
|
||||||
root = Path(config["transport_root"])
|
|
||||||
expected_peer = config["expected_peer_id"]
|
|
||||||
adapter = args.mtp_adapter
|
|
||||||
pull = subprocess.run(
|
|
||||||
[str(adapter), "pull", expected_peer, str(root)],
|
|
||||||
capture_output=True,
|
|
||||||
text=True,
|
|
||||||
check=False,
|
|
||||||
)
|
|
||||||
if pull.returncode != 0:
|
|
||||||
return 3
|
|
||||||
correlated_request = request_id(root / REQUEST_NAME)
|
|
||||||
if correlated_request is None:
|
|
||||||
return 3
|
|
||||||
if args.state.is_file():
|
|
||||||
try:
|
|
||||||
previous = json.loads(args.state.read_text(encoding="utf-8"))
|
|
||||||
if previous.get("request_id") == correlated_request:
|
|
||||||
return 3
|
|
||||||
except (OSError, UnicodeError, json.JSONDecodeError):
|
|
||||||
pass
|
|
||||||
run_id = "sy_" + str(uuid.uuid4())
|
|
||||||
now = datetime.now(timezone.utc).isoformat()
|
|
||||||
atomic_json(
|
|
||||||
args.state,
|
|
||||||
{
|
|
||||||
"format": "trainlog-sync-run-report",
|
|
||||||
"version": 1,
|
|
||||||
"run_id": run_id,
|
|
||||||
"request_id": correlated_request,
|
|
||||||
"trigger": args.trigger,
|
|
||||||
"requested_mode": "full_generation_v1",
|
|
||||||
"effective_mode": "full_generation_v1",
|
|
||||||
"phase": "requested",
|
|
||||||
"progress_revision": 1,
|
|
||||||
"result": "running",
|
|
||||||
"started_at": now,
|
|
||||||
"updated_at": now,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
result = subprocess.run(
|
|
||||||
[
|
|
||||||
sys.executable,
|
|
||||||
str(args.orchestrator),
|
|
||||||
"--database",
|
|
||||||
str(args.database),
|
|
||||||
"--state",
|
|
||||||
str(args.state),
|
|
||||||
"--config",
|
|
||||||
str(config_path),
|
|
||||||
"--run-id",
|
|
||||||
run_id,
|
|
||||||
"--request-id",
|
|
||||||
correlated_request,
|
|
||||||
],
|
|
||||||
capture_output=True,
|
|
||||||
text=True,
|
|
||||||
check=False,
|
|
||||||
)
|
|
||||||
append_log(result.stdout if result.returncode == 0 else result.stderr or result.stdout)
|
|
||||||
return result.returncode
|
|
||||||
|
|
||||||
|
|
||||||
def main() -> int:
|
def main() -> int:
|
||||||
parser = argparse.ArgumentParser()
|
parser = argparse.ArgumentParser()
|
||||||
|
|
||||||
|
|
@ -184,25 +55,6 @@ def main() -> int:
|
||||||
type=Path,
|
type=Path,
|
||||||
)
|
)
|
||||||
|
|
||||||
parser.add_argument("--trigger", choices=("android", "daemon"), default="android")
|
|
||||||
parser.add_argument(
|
|
||||||
"--generation-config",
|
|
||||||
type=Path,
|
|
||||||
default=os.environ.get("TRAINLOG_SYNC_GENERATION_CONFIG"),
|
|
||||||
)
|
|
||||||
parser.add_argument(
|
|
||||||
"--orchestrator",
|
|
||||||
type=Path,
|
|
||||||
default=Path(__file__).with_name("sync_orchestrator.py"),
|
|
||||||
)
|
|
||||||
parser.add_argument("--database", type=Path, default=default_database())
|
|
||||||
parser.add_argument("--state", type=Path)
|
|
||||||
parser.add_argument(
|
|
||||||
"--mtp-adapter",
|
|
||||||
type=Path,
|
|
||||||
default=Path(os.environ.get("TRAINLOG_SYNC_MTP_ADAPTER", "")),
|
|
||||||
)
|
|
||||||
|
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"--interval",
|
"--interval",
|
||||||
type=float,
|
type=float,
|
||||||
|
|
@ -211,20 +63,11 @@ def main() -> int:
|
||||||
|
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
|
||||||
if args.state is None:
|
|
||||||
# The Web adapter owns .sync-run.json. The daemon keeps independent
|
|
||||||
# durable request replay evidence while sharing the database lock.
|
|
||||||
args.state = Path(str(args.database) + ".syncd-run.json")
|
|
||||||
|
|
||||||
if not args.sync_once.exists():
|
if not args.sync_once.exists():
|
||||||
raise SystemExit(
|
raise SystemExit(
|
||||||
f"sync executable missing: {args.sync_once}"
|
f"sync executable missing: {args.sync_once}"
|
||||||
)
|
)
|
||||||
|
|
||||||
if args.generation_config is not None:
|
|
||||||
if not args.orchestrator.is_file() or not args.mtp_adapter.is_file():
|
|
||||||
raise SystemExit("full-generation runtime is incomplete")
|
|
||||||
|
|
||||||
signal.signal(
|
signal.signal(
|
||||||
signal.SIGTERM,
|
signal.SIGTERM,
|
||||||
request_stop,
|
request_stop,
|
||||||
|
|
@ -240,21 +83,17 @@ def main() -> int:
|
||||||
)
|
)
|
||||||
|
|
||||||
while not STOP:
|
while not STOP:
|
||||||
if args.generation_config is not None:
|
result = subprocess.run(
|
||||||
code = run_full_generation(args, args.generation_config)
|
[
|
||||||
result = subprocess.CompletedProcess([], code, "", "")
|
str(args.sync_once),
|
||||||
else:
|
"--request-only",
|
||||||
result = subprocess.run(
|
"--trigger",
|
||||||
[
|
"android",
|
||||||
str(args.sync_once),
|
],
|
||||||
"--request-only",
|
capture_output=True,
|
||||||
"--trigger",
|
text=True,
|
||||||
args.trigger,
|
check=False,
|
||||||
],
|
)
|
||||||
capture_output=True,
|
|
||||||
text=True,
|
|
||||||
check=False,
|
|
||||||
)
|
|
||||||
|
|
||||||
if result.returncode == 0:
|
if result.returncode == 0:
|
||||||
append_log(
|
append_log(
|
||||||
|
|
|
||||||
|
|
@ -653,12 +653,6 @@ test_generation_mtp = executable(
|
||||||
)
|
)
|
||||||
test('generation_mtp', test_generation_mtp)
|
test('generation_mtp', test_generation_mtp)
|
||||||
|
|
||||||
test(
|
|
||||||
'syncd_routing',
|
|
||||||
find_program('python3'),
|
|
||||||
args: [meson.project_source_root() / 'tests/test_syncd_routing.py'],
|
|
||||||
)
|
|
||||||
|
|
||||||
generation_mtp_adapter_double = executable(
|
generation_mtp_adapter_double = executable(
|
||||||
'generation-mtp-adapter-double',
|
'generation-mtp-adapter-double',
|
||||||
'tests/generation_mtp_adapter_double.c',
|
'tests/generation_mtp_adapter_double.c',
|
||||||
|
|
|
||||||
|
|
@ -338,16 +338,14 @@ static TrainlogStatus pull_referenced_generation(const TrainlogGenerationMtpIo *
|
||||||
TrainlogStatus status;
|
TrainlogStatus status;
|
||||||
status = one_named(io, selection, selection->root, "android-objects", true, &android_objects);
|
status = one_named(io, selection, selection->root, "android-objects", true, &android_objects);
|
||||||
if (status == TRAINLOG_STATUS_OK) {
|
if (status == TRAINLOG_STATUS_OK) {
|
||||||
status =
|
status = one_named(io, selection, android_objects.item_id, "generations", true, &generations);
|
||||||
one_named(io, selection, android_objects.item_id, "generations", true, &generations);
|
|
||||||
}
|
}
|
||||||
if (status == TRAINLOG_STATUS_OK) {
|
if (status == TRAINLOG_STATUS_OK) {
|
||||||
status = one_named(io, selection, generations.item_id, generation, true, &selected);
|
status = one_named(io, selection, generations.item_id, generation, true, &selected);
|
||||||
}
|
}
|
||||||
if (status != TRAINLOG_STATUS_OK ||
|
if (status != TRAINLOG_STATUS_OK ||
|
||||||
snprintf(path, sizeof(path), "%s/android-objects/generations/%s", local, generation) < 0 ||
|
snprintf(path, sizeof(path), "%s/android-objects/generations/%s", local, generation) < 0 ||
|
||||||
strlen(local) + strlen("/android-objects/generations/") + strlen(generation) >=
|
strlen(local) + strlen("/android-objects/generations/") + strlen(generation) >= sizeof(path)) {
|
||||||
sizeof(path)) {
|
|
||||||
return status == TRAINLOG_STATUS_OK ? TRAINLOG_STATUS_INVALID_ARGUMENT : status;
|
return status == TRAINLOG_STATUS_OK ? TRAINLOG_STATUS_INVALID_ARGUMENT : status;
|
||||||
}
|
}
|
||||||
char parent[1024];
|
char parent[1024];
|
||||||
|
|
@ -359,8 +357,7 @@ static TrainlogStatus pull_referenced_generation(const TrainlogGenerationMtpIo *
|
||||||
char objects_path[1024];
|
char objects_path[1024];
|
||||||
if (snprintf(objects_path, sizeof(objects_path), "%s/android-objects", local) < 0 ||
|
if (snprintf(objects_path, sizeof(objects_path), "%s/android-objects", local) < 0 ||
|
||||||
strlen(local) + strlen("/android-objects") >= sizeof(objects_path) ||
|
strlen(local) + strlen("/android-objects") >= sizeof(objects_path) ||
|
||||||
mkdir_private(objects_path) != TRAINLOG_STATUS_OK ||
|
mkdir_private(objects_path) != TRAINLOG_STATUS_OK || mkdir_private(parent) != TRAINLOG_STATUS_OK) {
|
||||||
mkdir_private(parent) != TRAINLOG_STATUS_OK) {
|
|
||||||
return TRAINLOG_STATUS_SYSTEM_ERROR;
|
return TRAINLOG_STATUS_SYSTEM_ERROR;
|
||||||
}
|
}
|
||||||
return pull_folder(io, selection, selected.item_id, path, 0U, &total, &objects);
|
return pull_folder(io, selection, selected.item_id, path, 0U, &total, &objects);
|
||||||
|
|
@ -590,17 +587,8 @@ TrainlogStatus trainlog_generation_mtp_pull(const TrainlogGenerationMtpIo *io,
|
||||||
* remain a normal polling state for the Python conversation.
|
* remain a normal polling state for the Python conversation.
|
||||||
* INVARIANT: no remote object, ACK, tombstone, or retained generation is
|
* INVARIANT: no remote object, ACK, tombstone, or retained generation is
|
||||||
* removed or rewritten by this bounded read. */
|
* removed or rewritten by this bounded read. */
|
||||||
status =
|
|
||||||
pull_named_file(io, &selection, selection.root, "android-peer-v1.json", local, true);
|
|
||||||
}
|
|
||||||
if (status == TRAINLOG_STATUS_OK) {
|
|
||||||
/* WHY: the Android request remains the explicit user/foreground
|
|
||||||
* admission signal even when the conversation uses full generations.
|
|
||||||
* CONTRACT: copy the bounded signal only; the orchestrator, not this
|
|
||||||
* transport adapter, owns correlation and all domain mutations.
|
|
||||||
* INVARIANT: reading the request never acknowledges or removes it. */
|
|
||||||
status = pull_named_file(
|
status = pull_named_file(
|
||||||
io, &selection, selection.root, "trainlog-sync-request-v1.json", local, false);
|
io, &selection, selection.root, "android-peer-v1.json", local, true);
|
||||||
}
|
}
|
||||||
if (status == TRAINLOG_STATUS_OK) {
|
if (status == TRAINLOG_STATUS_OK) {
|
||||||
status = pull_named_file(
|
status = pull_named_file(
|
||||||
|
|
|
||||||
|
|
@ -238,12 +238,6 @@ static int test_pull_and_manifest_last(void) {
|
||||||
"android-generation-v1.json",
|
"android-generation-v1.json",
|
||||||
"{\"relative_path\":\"android-objects/generations/gen_11111111-1111-4111-8111-"
|
"{\"relative_path\":\"android-objects/generations/gen_11111111-1111-4111-8111-"
|
||||||
"111111111111\"}");
|
"111111111111\"}");
|
||||||
add(13,
|
|
||||||
2,
|
|
||||||
false,
|
|
||||||
"trainlog-sync-request-v1.json",
|
|
||||||
"{\"format\":\"trainlog-sync-request\",\"version\":1,\"request_id\":\"sr_11111111-"
|
|
||||||
"1111-4111-8111-111111111111\",\"requested_at\":\"2026-09-20T12:00:00Z\"}");
|
|
||||||
add(5, 2, true, "android-objects", NULL);
|
add(5, 2, true, "android-objects", NULL);
|
||||||
add(6, 5, true, "generations", NULL);
|
add(6, 5, true, "generations", NULL);
|
||||||
add(7, 6, true, "gen_11111111-1111-4111-8111-111111111111", NULL);
|
add(7, 6, true, "gen_11111111-1111-4111-8111-111111111111", NULL);
|
||||||
|
|
@ -261,8 +255,6 @@ static int test_pull_and_manifest_last(void) {
|
||||||
root,
|
root,
|
||||||
"android-objects/generations/gen_11111111-1111-4111-8111-111111111111/payload.json"));
|
"android-objects/generations/gen_11111111-1111-4111-8111-111111111111/payload.json"));
|
||||||
CHECK(access(path, R_OK) == 0);
|
CHECK(access(path, R_OK) == 0);
|
||||||
CHECK(trainlog_test_join_path(path, sizeof(path), root, "trainlog-sync-request-v1.json"));
|
|
||||||
CHECK(access(path, R_OK) == 0);
|
|
||||||
CHECK(trainlog_test_join_path(path, sizeof(path), root, "out"));
|
CHECK(trainlog_test_join_path(path, sizeof(path), root, "out"));
|
||||||
CHECK(mkdir(path, 0700) == 0);
|
CHECK(mkdir(path, 0700) == 0);
|
||||||
char gen[512];
|
char gen[512];
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue