P2.3: publish enforces channel-name target + release-id uniqueness
Close the remaining publish aggregate-consistency edges (review round 2, F1 continued): - chan.name == release.channel — the promoted channel must be the one the release declares as its target; promoting a "beta" channel for a release whose channel is "stable" committed an edge contradicting the release's own target. Now rejected with Integrity + rollback. - release.id must be new — a colliding id would shadow the existing release (get_release resolves to the OLD one), so the channel edge would silently point at a different release than the one published. Now rejected with Integrity + rollback. tests/repo_transaction.sx: add a channel-name-mismatch case and a release-id-collision case (both assert Integrity + model unchanged); existing fully-consistent publish still commits. Both new cases fail on the pre-fix repo.sx and pass after.
This commit is contained in:
@@ -6,13 +6,17 @@
|
||||
// Flow:
|
||||
// 1. Seed via a SUCCESSFUL publish (rel_00 + art_00, channel stable ->
|
||||
// rel_00) — exercises the commit path.
|
||||
// 2. A publish whose 2nd artifact is invalid must RAISE and roll back:
|
||||
// counts unchanged, channel still -> rel_00, rel_01 absent, the (valid)
|
||||
// first artifact absent too.
|
||||
// 3. A publish whose artifact names the wrong release_id must RAISE
|
||||
// Integrity and roll back identically.
|
||||
// 4. Persist + reload: the rolled-back state is what hits db.json — the
|
||||
// reloaded channel still points at rel_00 and rel_01 is absent.
|
||||
// 2. A publish whose 2nd artifact is invalid must RAISE Validation and roll
|
||||
// back: counts unchanged, channel still -> rel_00, nothing inserted.
|
||||
// 3..7. Each Integrity precondition rejects + rolls back identically:
|
||||
// (3) artifact.release_id mismatch, (4) cross-app channel
|
||||
// (chan.app_id != release.app_id), (5) cross-app artifact
|
||||
// (artifact.app_id != release.app_id), (6) channel-NAME mismatch
|
||||
// (chan.name != release.channel), (7) release-id collision
|
||||
// (release.id already exists).
|
||||
// 8. Persist + reload: the rolled-back state is what hits db.json — the
|
||||
// reloaded channel still points at rel_00 and the attempted releases
|
||||
// are absent.
|
||||
// Uses a fresh `<root>` under `.sx-tmp/` and cleans up.
|
||||
#import "modules/std.sx";
|
||||
process :: #import "modules/process.sx";
|
||||
@@ -50,8 +54,11 @@ the_channel :: () -> Channel {
|
||||
return mk_channel_for("app_01");
|
||||
}
|
||||
mk_channel_for :: (app_id: string) -> Channel {
|
||||
return mk_named_channel(app_id, "stable");
|
||||
}
|
||||
mk_named_channel :: (app_id: string, name: string) -> Channel {
|
||||
return Channel.{
|
||||
app_id = app_id, name = "stable", current_release_id = "",
|
||||
app_id = app_id, name = name, current_release_id = "",
|
||||
policy = .manual, rollout_percent = 100,
|
||||
};
|
||||
}
|
||||
@@ -180,7 +187,57 @@ main :: () -> s32 {
|
||||
process.assert(cyv.current_release_id == "rel_00", "cross-app artifact rollback: channel still -> rel_00");
|
||||
print(" cross-app artifact rejected: model unchanged\n");
|
||||
|
||||
// ── 6. Persisted state reflects the rollback, not the attempts ───
|
||||
// ── 6. Channel-NAME mismatch: chan.name != release.channel ───────
|
||||
// The release declares channel = "stable" as its target, but a channel
|
||||
// NAMED "beta" (same app) is promoted. Committing would point the beta
|
||||
// channel at a release that names stable as its target — an edge that
|
||||
// contradicts the release's own declared channel. Must raise Integrity.
|
||||
arts_cn : List(Artifact) = .{};
|
||||
arts_cn.append(mk_artifact("art_cn", "rel_cn", DIGEST_B)); // self-consistent artifact
|
||||
cn_failed := false;
|
||||
cn_integrity := false;
|
||||
repo.publish(mk_release("rel_cn", "1.5.0"), @arts_cn, mk_named_channel("app_01", "beta")) catch e {
|
||||
cn_failed = true;
|
||||
cn_integrity = (e == error.Integrity);
|
||||
};
|
||||
process.assert(cn_failed, "channel-name mismatch must fail");
|
||||
process.assert(cn_integrity, "channel-name mismatch must raise Integrity");
|
||||
process.assert(repo.releases.len == rel_n, "channel-name rollback: release count unchanged");
|
||||
process.assert(repo.artifacts.len == art_n, "channel-name rollback: artifact count unchanged");
|
||||
process.assert(repo.channels.len == chan_n, "channel-name rollback: channel count unchanged");
|
||||
process.assert(repo.get_release("rel_cn") == null, "channel-name rollback: release absent");
|
||||
process.assert(repo.get_channel("app_01", "beta") == null, "channel-name rollback: no beta channel created");
|
||||
ccn := repo.get_channel("app_01", "stable");
|
||||
process.assert(ccn != null, "channel-name rollback: stable channel still exists");
|
||||
ccnv := ccn!;
|
||||
process.assert(ccnv.current_release_id == "rel_00", "channel-name rollback: stable channel still -> rel_00");
|
||||
print(" channel-name mismatch rejected: model unchanged\n");
|
||||
|
||||
// ── 7. Release-id collision: a publish must introduce a NEW release ─
|
||||
// Re-publishing rel_00's id would shadow the existing release: get_release
|
||||
// resolves to the OLD one, so the channel edge would silently point at a
|
||||
// release other than the one being published. Must raise Integrity and
|
||||
// leave the original rel_00 untouched.
|
||||
arts_dup : List(Artifact) = .{};
|
||||
arts_dup.append(mk_artifact("art_dup", "rel_00", DIGEST_B));
|
||||
dup_failed := false;
|
||||
dup_integrity := false;
|
||||
repo.publish(mk_release("rel_00", "9.9.9"), @arts_dup, the_channel()) catch e {
|
||||
dup_failed = true;
|
||||
dup_integrity = (e == error.Integrity);
|
||||
};
|
||||
process.assert(dup_failed, "republishing an existing release id must fail");
|
||||
process.assert(dup_integrity, "release-id collision must raise Integrity");
|
||||
process.assert(repo.releases.len == rel_n, "collision rollback: release count unchanged");
|
||||
process.assert(repo.artifacts.len == art_n, "collision rollback: artifact count unchanged");
|
||||
process.assert(repo.channels.len == chan_n, "collision rollback: channel count unchanged");
|
||||
rdup := repo.get_release("rel_00");
|
||||
process.assert(rdup != null, "collision: original rel_00 still present");
|
||||
rdupv := rdup!;
|
||||
process.assert(rdupv.version == "1.0.0", "collision: original release not overwritten");
|
||||
print(" release-id collision rejected: original release untouched\n");
|
||||
|
||||
// ── 8. Persisted state reflects the rollback, not the attempts ───
|
||||
serr := false;
|
||||
save(repo, root) catch { serr = true; };
|
||||
process.assert(!serr, "save must succeed");
|
||||
|
||||
Reference in New Issue
Block a user