test(sync): close lifecycle validation evidence
This commit is contained in:
parent
3db84bbfb0
commit
4b8f5b02b8
7 changed files with 295 additions and 13 deletions
|
|
@ -10,6 +10,14 @@
|
|||
remains V3; no service, user database, phone, tag or release artifact was
|
||||
changed or deployed.
|
||||
|
||||
- Closed the lifecycle validation evidence with real Android/desktop V4
|
||||
history and execution-draft round trips in both producer directions. Fixed
|
||||
Android V4 import so validated target, load, and rest fields are persisted,
|
||||
reconciled the 73/75 native count to the two `web=auto` frontend tests, and
|
||||
documented identical pre-existing GCC 16 `-Wmisleading-indentation`
|
||||
failures on baseline and branch while retaining Clang 22 strict green
|
||||
builds. V4 remains staged and inactive.
|
||||
|
||||
- Completed `TRAINLOG_SYNC_CHARACTERIZATION_V1` without changing production
|
||||
synchronization, schemas, or formats. Added a real Android V3 exporter →
|
||||
desktop importer/exporter → fresh Android importer round trip, and completed
|
||||
|
|
|
|||
|
|
@ -4072,7 +4072,7 @@ class TrainlogRepository(
|
|||
put("entry_id", entryId); put("session_row_id", rowId); put("exercise_row_id", exerciseRow.rowId)
|
||||
put("position", position); put("recording_mode", recording); put("tracking_mode", tracking)
|
||||
put("data_fields", entry.optInt("data_fields", 0))
|
||||
if (version == 3) {
|
||||
if (version >= 3) {
|
||||
val target = entry.optJSONObject("target")
|
||||
put("load_mode", entry.getString("load_mode"))
|
||||
put("rest_seconds", entry.getInt("rest_seconds"))
|
||||
|
|
|
|||
|
|
@ -38,6 +38,8 @@ import org.robolectric.RobolectricTestRunner
|
|||
import org.robolectric.annotation.Config
|
||||
import java.io.File
|
||||
import java.nio.file.Files
|
||||
import java.time.OffsetDateTime
|
||||
import java.time.ZoneOffset
|
||||
import java.util.UUID
|
||||
|
||||
@RunWith(RobolectricTestRunner::class)
|
||||
|
|
@ -507,6 +509,146 @@ class TrainlogRepositoryDraftTest {
|
|||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun v4HistoryRoundTripsAcrossRealAndroidAndDesktopImplementations() {
|
||||
val sourceName = "v4-source-${UUID.randomUUID()}.db"
|
||||
val destinationName = "v4-destination-${UUID.randomUUID()}.db"
|
||||
val source = TrainlogRepository(context, sourceName)
|
||||
val destination = TrainlogRepository(context, destinationName)
|
||||
val root = Files.createTempDirectory("trainlog-v4-roundtrip-").toFile()
|
||||
try {
|
||||
val exercise = createExercise(source, "V4 échange", RecordingMode.SETS, TrackingMode.REPS)
|
||||
val entryId = "sxe_40000000-0000-4000-8000-000000000001"
|
||||
assertTrue(source.saveSession(SessionDraft(listOf(SessionExerciseDraft(
|
||||
entryId = entryId, exercise = exercise, equipmentId = "leg_press",
|
||||
plan = SessionExercisePlan(2, reps = 8, weightKg = 42.5,
|
||||
loadMode = SessionLoadMode.EXTERNAL, restSeconds = 90),
|
||||
sets = listOf(SessionSetDraft(reps = 8, weightKg = 40.0),
|
||||
SessionSetDraft(reps = 6, weightKg = 42.5)),
|
||||
)))) is SaveSessionResult.Saved)
|
||||
val sourceArtifact = JSONObject(source.buildMobileExportV4Json())
|
||||
val session = sourceArtifact.getJSONArray("sessions").getJSONObject(0)
|
||||
val sessionId = session.getString("session_id")
|
||||
assertTrue(source.updateSyncNote("session", sessionId,
|
||||
"quoted \"note\" \\ newline\né") is SyncNoteEditResult.Saved)
|
||||
assertTrue(source.updateSyncNote("occurrence", entryId, "x".repeat(4096))
|
||||
is SyncNoteEditResult.Saved)
|
||||
assertEquals(SyncNoteEditResult.Invalid,
|
||||
source.updateSyncNote("occurrence", entryId, "x".repeat(4097)))
|
||||
val observation = source.saveBodyObservation(
|
||||
BodyObservationDraft(bodyWeightKg = 81.25, waistCm = 84.0),
|
||||
) as SaveBodyObservationResult.Saved
|
||||
assertTrue(source.linkBodyObservationToSession(observation.observationId, sessionId))
|
||||
assertTrue(source.updateSyncNote("observation", observation.observationId, "")
|
||||
is SyncNoteEditResult.Saved)
|
||||
val androidExport = File(root, "android-v4.json")
|
||||
val completed = JSONObject(source.buildMobileExportV4Json())
|
||||
val completedSession = completed.getJSONArray("sessions").getJSONObject(0)
|
||||
val started = OffsetDateTime.parse(completedSession.getString("started_at"))
|
||||
completedSession.put("ended_at", started.plusMinutes(1).toString())
|
||||
androidExport.writeText(completed.toString())
|
||||
val desktopDatabase = File(root, "desktop.sqlite")
|
||||
val desktopExport = File(root, "desktop-v4.json")
|
||||
runLifecycleBridge("history", androidExport, desktopDatabase, desktopExport)
|
||||
|
||||
val returned = JSONObject(desktopExport.readText())
|
||||
val catalog = JSONObject().put("format", "trainlog-pc-catalog").put("version", 1)
|
||||
.put("exercises", JSONArray(returned.getJSONArray("exercises").toString()))
|
||||
assertTrue(destination.applyPcCatalogJson(catalog.toString()) is PcCatalogImportResult.Applied)
|
||||
val equalEnd = JSONObject(returned.toString())
|
||||
equalEnd.getJSONArray("sessions").getJSONObject(0)
|
||||
.put("ended_at", started.toString())
|
||||
assertTrue(destination.applyPcMobileExportV4Json(equalEnd.toString())
|
||||
is MobileSessionImportResult.Invalid)
|
||||
val equivalentOffset = JSONObject(returned.toString())
|
||||
equivalentOffset.getJSONArray("sessions").getJSONObject(0)
|
||||
.put("ended_at", started.withOffsetSameInstant(ZoneOffset.UTC).toString())
|
||||
assertTrue(destination.applyPcMobileExportV4Json(equivalentOffset.toString())
|
||||
is MobileSessionImportResult.Invalid)
|
||||
assertEquals(MobileSessionImportResult.Applied(1, 0, 1, 0),
|
||||
destination.applyPcMobileExportV4Json(returned.toString()))
|
||||
assertEquals(MobileSessionImportResult.Applied(0, 1, 0, 1),
|
||||
destination.applyPcMobileExportV4Json(returned.toString()))
|
||||
val detail = destination.getSessionDetail(sessionId)!!.exercises.single()
|
||||
assertEquals(entryId, detail.entryId)
|
||||
assertEquals(90, detail.plan!!.restSeconds)
|
||||
assertEquals(listOf(8, 6), detail.sets.map { it.reps })
|
||||
assertEquals(listOf(40.0, 42.5), detail.sets.map { it.weightKg })
|
||||
assertEquals(sessionId, JSONObject(destination.buildMobileExportV4Json())
|
||||
.getJSONArray("body_observations").getJSONObject(0).getString("session_id"))
|
||||
assertEquals(semanticV4Projection(JSONObject(androidExport.readText())),
|
||||
semanticV4Projection(JSONObject(destination.buildMobileExportV4Json())))
|
||||
|
||||
val secondDesktop = File(root, "desktop-fresh.sqlite")
|
||||
val finalDesktopExport = File(root, "desktop-final-v4.json")
|
||||
val destinationExport = File(root, "destination-v4.json")
|
||||
destinationExport.writeText(destination.buildMobileExportV4Json())
|
||||
runLifecycleBridge("history", destinationExport, secondDesktop, finalDesktopExport)
|
||||
assertEquals(semanticV4Projection(JSONObject(destinationExport.readText())),
|
||||
semanticV4Projection(JSONObject(finalDesktopExport.readText())))
|
||||
} finally {
|
||||
source.close(); destination.close()
|
||||
context.deleteDatabase(sourceName); context.deleteDatabase(destinationName)
|
||||
root.deleteRecursively()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun executionDraftRoundTripsAcrossRealAndroidAndDesktopImplementations() {
|
||||
val sourceName = "draft-source-${UUID.randomUUID()}.db"
|
||||
val destinationName = "draft-destination-${UUID.randomUUID()}.db"
|
||||
val source = TrainlogRepository(context, sourceName)
|
||||
val destination = TrainlogRepository(context, destinationName)
|
||||
val root = Files.createTempDirectory("trainlog-draft-roundtrip-").toFile()
|
||||
try {
|
||||
val exercise = createExercise(source, "Draft bridge", RecordingMode.SETS, TrackingMode.REPS)
|
||||
val entryId = "sxe_50000000-0000-4000-8000-000000000001"
|
||||
assertEquals(ActiveDraftMutationResult.Saved, source.saveActiveSessionDraft(
|
||||
ActiveSessionDraft(exercises = listOf(SessionExerciseDraft(
|
||||
entryId = entryId, exercise = exercise,
|
||||
plan = SessionExercisePlan(3, reps = 7, restSeconds = 75),
|
||||
sets = listOf(SessionSetDraft(reps = 7)),
|
||||
)), form = SessionDraftForm(weightText = "unfinished 42.")),
|
||||
))
|
||||
val initial = JSONObject(source.buildExecutionDraftExportV1Json())
|
||||
assertFalse(initial.toString().contains("unfinished 42."))
|
||||
val draft = initial.getJSONArray("drafts").getJSONObject(0)
|
||||
val sessionId = draft.getString("session_id")
|
||||
val androidExport = File(root, "android-draft.json").apply { writeText(initial.toString()) }
|
||||
val desktopDatabase = File(root, "desktop.sqlite")
|
||||
val desktopExport = File(root, "desktop-draft.json")
|
||||
runLifecycleBridge("draft", androidExport, desktopDatabase, desktopExport)
|
||||
|
||||
val catalog = JSONObject().put("format", "trainlog-pc-catalog").put("version", 1)
|
||||
.put("exercises", JSONObject(source.buildMobileExportV4Json()).getJSONArray("exercises"))
|
||||
assertTrue(destination.applyPcCatalogJson(catalog.toString()) is PcCatalogImportResult.Applied)
|
||||
assertEquals(ExecutionDraftImportResult.Applied(1, 0, 0, 0),
|
||||
destination.applyExecutionDraftExportV1Json(desktopExport.readText()))
|
||||
assertEquals(ExecutionDraftImportResult.Applied(0, 0, 1, 0),
|
||||
destination.applyExecutionDraftExportV1Json(desktopExport.readText()))
|
||||
val destinationArtifact = File(root, "destination-draft.json").apply {
|
||||
writeText(destination.buildExecutionDraftExportV1Json())
|
||||
}
|
||||
val freshDesktop = File(root, "desktop-fresh.sqlite")
|
||||
val finalArtifact = File(root, "desktop-final-draft.json")
|
||||
runLifecycleBridge("draft", destinationArtifact, freshDesktop, finalArtifact)
|
||||
assertEquals(semanticDraftProjection(JSONObject(androidExport.readText())),
|
||||
semanticDraftProjection(JSONObject(finalArtifact.readText())))
|
||||
assertEquals(FinalizeActiveDraftResult.Saved(sessionId),
|
||||
destination.finalizeExecutionDraft(sessionId))
|
||||
destination.close()
|
||||
val reopened = TrainlogRepository(context, destinationName)
|
||||
assertEquals(ExecutionDraftImportResult.Applied(0, 0, 0, 1),
|
||||
reopened.applyExecutionDraftExportV1Json(desktopExport.readText()))
|
||||
reopened.close()
|
||||
} finally {
|
||||
source.close()
|
||||
try { destination.close() } catch (_: Exception) { }
|
||||
context.deleteDatabase(sourceName); context.deleteDatabase(destinationName)
|
||||
root.deleteRecursively()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun repeatedContinuousExercisePersistsDistinctOccurrencesAcrossReopenAndFinalize() {
|
||||
val repo = openRepository()
|
||||
|
|
@ -2583,6 +2725,34 @@ class TrainlogRepositoryDraftTest {
|
|||
return canonicalJson(copy).toString()
|
||||
}
|
||||
|
||||
private fun semanticV4Projection(value: JSONObject): String =
|
||||
semanticV3Projection(value)
|
||||
|
||||
private fun semanticDraftProjection(value: JSONObject): String {
|
||||
val copy = JSONObject(value.toString())
|
||||
copy.remove("generated_at")
|
||||
val source = copy.getJSONArray("drafts")
|
||||
val sorted = (0 until source.length()).map { source.getJSONObject(it) }
|
||||
.sortedBy { it.getString("session_id") }
|
||||
copy.put("drafts", JSONArray().also { output -> sorted.forEach(output::put) })
|
||||
return canonicalJson(copy).toString()
|
||||
}
|
||||
|
||||
private fun runLifecycleBridge(
|
||||
kind: String,
|
||||
input: File,
|
||||
database: File,
|
||||
output: File,
|
||||
) {
|
||||
val bridge = findRepositoryFile("tests/support/roundtrip_desktop_lifecycle.py")
|
||||
val process = ProcessBuilder(
|
||||
"python3", bridge.absolutePath, kind, input.absolutePath,
|
||||
database.absolutePath, output.absolutePath,
|
||||
).redirectErrorStream(true).start()
|
||||
val text = process.inputStream.bufferedReader().readText()
|
||||
assertEquals("desktop lifecycle bridge failed: $text", 0, process.waitFor())
|
||||
}
|
||||
|
||||
private fun canonicalJson(value: Any?): Any = when (value) {
|
||||
is JSONObject -> JSONObject().also { normalized ->
|
||||
value.keys().asSequence().toList().sorted().forEach { key ->
|
||||
|
|
|
|||
|
|
@ -223,6 +223,17 @@ four skips remain the documented optional historical v9, v12, v13, and v14
|
|||
database fixtures. No hardware MTP, real Drive, adb, instrumented device test,
|
||||
user database, exchange directory, or user service participated.
|
||||
|
||||
For the `TRAINLOG_SYNC_DATA_LIFECYCLE_V1` closeout on 2026-09-17, real staged
|
||||
V4 history and execution-draft entry points completed both Android/desktop
|
||||
producer directions through fresh destination databases. The closeout fixed
|
||||
one Android V4 import defect that validated but failed to persist occurrence
|
||||
plan fields. Isolated validation passed **75/75 normal Meson tests**,
|
||||
**75/75 ASan/UBSan Meson tests**, Android debug assembly, and **207 Android
|
||||
tests (203 passed, 4 skipped, 0 failed/error)**. The four skips remain the
|
||||
optional real historical v9, v12, v13, and v14 database fixtures. The active
|
||||
transport remains V3; no phone, MTP, Drive, service, user database, deployment,
|
||||
or instrumented device test participated.
|
||||
|
||||
The Dashboard Core extraction tranche passes **62/62 normal Meson tests** and
|
||||
**62/62 ASan/UBSan Meson tests**, including the standalone public-header and
|
||||
Core characterization targets, plus both canonical JSON validators.
|
||||
|
|
|
|||
|
|
@ -443,6 +443,14 @@ on the real private-XDG lock. The assertion-level matrix is owned by
|
|||
tombstones, coherent generations, causal merge, or a peer-consumption
|
||||
acknowledgement.
|
||||
|
||||
**[CURRENT EVIDENCE]** `TRAINLOG_SYNC_DATA_LIFECYCLE_V1=PASS/FROZEN` now has
|
||||
real cross-implementation round trips for enriched mobile V4 history and the
|
||||
separate execution-draft V1 artifact in both producer directions. The exact
|
||||
entry points, assertions, commands, compiler comparison, test inventory, and
|
||||
remaining transport boundary are recorded in `docs/tests.md`. This evidence
|
||||
does not select V4 transport and does not implement causal deletion,
|
||||
generation manifests, peer-consumption acknowledgement, or orchestration.
|
||||
|
||||
## 16. Sources propriétaires
|
||||
|
||||
- état/roadmap : `docs/current_state.md`, `docs/roadmap.md` ;
|
||||
|
|
|
|||
|
|
@ -402,11 +402,27 @@ python3 tools/validate_sync_isolated.py --suite full --jdk /usr/lib/jvm/java-17-
|
|||
`TRAINLOG_SYNC_DATA_LIFECYCLE_V1` adds schema v19/v18 migration assertions and
|
||||
Android lifecycle coverage for confirmed-only serialization, persistent
|
||||
pending state, occupied-singleton refusal, explicit activation, identity-
|
||||
preserving idempotent finalization and stale replay. The full isolated run at
|
||||
closure passed 73/73 normal native tests, 73/73 ASan/UBSan native tests and 205
|
||||
Android tests (201 passed, four unchanged historical-fixture skips), plus both
|
||||
Python validators and `assembleDebug`. These are artifact/repository proofs;
|
||||
they do not claim transport activation or global synchronization atomicity.
|
||||
preserving idempotent finalization and stale replay. The closeout adds
|
||||
`v4HistoryRoundTripsAcrossRealAndroidAndDesktopImplementations` and
|
||||
`executionDraftRoundTripsAcrossRealAndroidAndDesktopImplementations`. Their
|
||||
bridge provisions only synthetic schema and invokes the real Android and
|
||||
desktop staged entry points; it does not reconstruct wire data. The final
|
||||
isolated run passed 75/75 normal native tests, 75/75 ASan/UBSan native tests
|
||||
and 207 Android tests (203 passed, four unchanged historical-fixture skips),
|
||||
plus both Python validators and `assembleDebug`. These are artifact/repository
|
||||
proofs; they do not claim transport activation or global synchronization
|
||||
atomicity.
|
||||
|
||||
The 75-versus-73 inventory contains no removed, renamed, merged, or disabled
|
||||
native tests. With `web=auto` and no `web/node_modules`, Meson selects 73 tests
|
||||
and omits exactly `web_frontend_typecheck` and `web_frontend`. After `npm ci`
|
||||
from the locked package file and Meson reconfiguration, both the normal and
|
||||
ASan/UBSan selections contain and pass all 75. Clang 22.1.8 is the compiler
|
||||
used for those green builds. GCC 16.2.1 fails both baseline `77c1517` and the
|
||||
lifecycle branch under the unchanged strict `-Werror` policy on
|
||||
`-Wmisleading-indentation` in unchanged `json_writer.c`,
|
||||
`web_dashboard_json.c`, and `web_dashboard.c`; this is a pre-existing
|
||||
compiler-version limitation, not lifecycle evidence and not a relaxed gate.
|
||||
|
||||
Use `--jdk /absolute/jdk17/home` when auto-detection is unsuitable and
|
||||
`--run-parent /absolute/private/parent` to select another parent outside the
|
||||
|
|
@ -695,14 +711,16 @@ passing tests:
|
|||
| Request, receipt, and processed marker | `sync_body_zone_wiring` through `trainlog_sync_run()` | Receipt and report carry the exact `request_id`; identical request replay is refused; receipt-send failure leaves the prior processed marker unchanged | Added |
|
||||
| Android post-receipt consumption | Desktop request/receipt proof plus Android typed receipt/inbox component tests | Desktop processing success and Android artifact-import success are separate component results; V1 has no durable peer-consumption acknowledgement | Component-scoped only |
|
||||
| Inter-process lock | `sync_body_zone_wiring` against the real `sync.lock` acquisition with private `XDG_DATA_HOME` | A pipe-coordinated child holds the lock; the second trigger returns conflict before any MTP probe; every child is joined | Added |
|
||||
| V4 drafts, general tombstones, coherent generations, causal merge, and consumption acknowledgement | Frozen gap contract only | No active production implementation exists | Future criterion, not tested green |
|
||||
| Enriched V4 completed history | `TrainlogRepositoryDraftTest.v4HistoryRoundTripsAcrossRealAndroidAndDesktopImplementations` through Android V4 repository methods and the real desktop V4 importer/exporter | Android → desktop → fresh Android and desktop → Android → fresh desktop preserve stable identities, order, heterogeneous sets, plans, equipment, causal null/empty/escaped UTF-8 notes and idempotent replay | Added cross-implementation proof |
|
||||
| Execution-draft V1 | `TrainlogRepositoryDraftTest.executionDraftRoundTripsAcrossRealAndroidAndDesktopImplementations` through Android repository methods and `execution_draft_exchange.py` | Both implementation directions preserve stable session/entry identity and confirmed facts; raw unfinished form text remains local; replay is unchanged and pre-finalization replay is stale after reopen/finalization | Added cross-implementation proof |
|
||||
| General tombstones, coherent generations, causal deletion, and consumption acknowledgement | Frozen gap contract only | No active production implementation exists | Future criterion, not tested green |
|
||||
|
||||
The cross-implementation helper only provisions the established minimal desktop
|
||||
test schema and launches the production Python tools; it does not duplicate
|
||||
their business rules. The Kotlin test uses the real Android V3 exporter and
|
||||
importer on two distinct databases. Its comparison removes only the volatile
|
||||
`generated_at`, canonicalizes object-key and unordered top-level collection
|
||||
order, and retains occurrence/set array order as business data.
|
||||
The cross-implementation helpers only provision minimal production-compatible
|
||||
desktop schemas and launch production Python tools; they do not duplicate
|
||||
their business rules. Kotlin tests use real Android exporters and importers on
|
||||
distinct databases. Comparisons remove only volatile `generated_at`,
|
||||
canonicalize object-key and unordered top-level collection order, and retain
|
||||
occurrence/set array order as business data.
|
||||
|
||||
Request/receipt evidence remains deliberately bounded: the native test proves
|
||||
desktop correlation, publication, replay handling, and marking order, while
|
||||
|
|
|
|||
67
tests/support/roundtrip_desktop_lifecycle.py
Normal file
67
tests/support/roundtrip_desktop_lifecycle.py
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Bridge lifecycle artifacts through the real staged desktop entry points."""
|
||||
|
||||
import argparse
|
||||
import sqlite3
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
sys.path.insert(0, str(ROOT / "tests"))
|
||||
|
||||
from test_session_exchange_v3 import V11_SCHEMA # noqa: E402
|
||||
|
||||
|
||||
V19_ADDITIONS = """
|
||||
ALTER TABLE session_exercises ADD COLUMN tracking_mode TEXT;
|
||||
CREATE TABLE exercise_aliases(source_exercise_id TEXT PRIMARY KEY,canonical_exercise_id TEXT NOT NULL REFERENCES exercises(exercise_id) ON DELETE RESTRICT,CHECK(source_exercise_id<>canonical_exercise_id));
|
||||
CREATE TABLE exercise_feedback(id INTEGER PRIMARY KEY,feedback_id TEXT NOT NULL UNIQUE,session_exercise_row_id INTEGER NOT NULL REFERENCES session_exercises(id) ON DELETE CASCADE,observed_at TEXT NOT NULL,raw_text TEXT NOT NULL);
|
||||
CREATE TABLE exercise_feedback_revisions(revision_id TEXT PRIMARY KEY,feedback_id TEXT NOT NULL REFERENCES exercise_feedback(feedback_id) ON DELETE CASCADE,created_at TEXT NOT NULL,raw_text TEXT NOT NULL);
|
||||
CREATE TABLE sync_note_revisions(owner_kind TEXT NOT NULL,owner_id TEXT NOT NULL,revision_id TEXT NOT NULL,parent_revision_id TEXT,note TEXT,CHECK(note IS NULL OR length(CAST(note AS BLOB))<=4096),PRIMARY KEY(owner_kind,owner_id,revision_id));
|
||||
CREATE TABLE sync_note_state(owner_kind TEXT NOT NULL,owner_id TEXT NOT NULL,revision_id TEXT NOT NULL,PRIMARY KEY(owner_kind,owner_id),FOREIGN KEY(owner_kind,owner_id,revision_id) REFERENCES sync_note_revisions(owner_kind,owner_id,revision_id));
|
||||
CREATE TABLE execution_drafts(session_id TEXT PRIMARY KEY,session_type TEXT NOT NULL,source_session_id TEXT,started_at TEXT,revision_id TEXT NOT NULL,parent_revision_id TEXT,state TEXT NOT NULL,payload_json TEXT NOT NULL);
|
||||
CREATE TABLE execution_draft_revisions(session_id TEXT NOT NULL,revision_id TEXT NOT NULL,parent_revision_id TEXT,payload_json TEXT NOT NULL,PRIMARY KEY(session_id,revision_id));
|
||||
CREATE TABLE execution_draft_finalizations(session_id TEXT PRIMARY KEY,final_revision_id TEXT NOT NULL,finalized_at TEXT NOT NULL);
|
||||
PRAGMA user_version=19;
|
||||
"""
|
||||
|
||||
|
||||
def run(arguments: list[str]) -> None:
|
||||
result = subprocess.run(arguments, text=True, capture_output=True, check=False)
|
||||
if result.returncode != 0:
|
||||
raise RuntimeError(result.stdout + result.stderr)
|
||||
|
||||
|
||||
def create_v19(path: Path) -> None:
|
||||
# CONTRACT: fixture provisioning supplies only the production schema. All
|
||||
# artifact semantics remain owned by the real desktop tools below.
|
||||
with sqlite3.connect(path) as connection:
|
||||
connection.executescript(V11_SCHEMA)
|
||||
connection.executescript(V19_ADDITIONS)
|
||||
|
||||
|
||||
def main() -> None:
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("kind", choices=("history", "draft"))
|
||||
parser.add_argument("input", type=Path)
|
||||
parser.add_argument("database", type=Path)
|
||||
parser.add_argument("output", type=Path)
|
||||
args = parser.parse_args()
|
||||
create_v19(args.database)
|
||||
if args.kind == "history":
|
||||
run([sys.executable, str(ROOT / "tools/import_mobile_export.py"),
|
||||
str(args.input), "--database", str(args.database)])
|
||||
run([sys.executable, str(ROOT / "tools/export_pc_mobile.py"),
|
||||
str(args.output), "--database", str(args.database), "--version", "4"])
|
||||
else:
|
||||
tool = str(ROOT / "tools/execution_draft_exchange.py")
|
||||
run([sys.executable, tool, "import", str(args.input),
|
||||
"--database", str(args.database)])
|
||||
run([sys.executable, tool, "export", str(args.output),
|
||||
"--database", str(args.database)])
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Loading…
Reference in a new issue