rootfs: the series copy gate (Plan 1 Task 1)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2 files changed,
+255
−3
approved_moves_copy_test.go+13 −3approved_rootfs_copy_test.go+242 −0
CI passed — run details
CI log
=== gate ===
.amadan/test-gate.sh
=== launchctx (1 s)
ok amadan.net/carlos/platform.git/launchctx 0.012s
? amadan.net/carlos/platform.git/launchctx/controlname [no test files]
? amadan.net/carlos/platform.git/launchctx/wireid [no test files]
=== vet (18 s)
=== gofmt (1 s)
gofmt -l .: clean
=== rest (176 s)
? github.com/carlosframework/platform/cmd/changelog [no test files]
ok github.com/carlosframework/platform/cmd/console 4.066s
ok github.com/carlosframework/platform/cmd/moveprobe 11.861s
ok github.com/carlosframework/platform/cmd/zddprobe 0.796s
ok github.com/carlosframework/platform/internal/activator 59.700s
ok github.com/carlosframework/platform/internal/agentsock 0.041s
ok github.com/carlosframework/platform/internal/alerting 0.010s
ok github.com/carlosframework/platform/internal/analytics 0.044s
ok github.com/carlosframework/platform/internal/appconfig 3.232s
ok github.com/carlosframework/platform/internal/apperr 0.165s
ok github.com/carlosframework/platform/internal/applog 0.271s
ok github.com/carlosframework/platform/internal/awsinv 0.010s
ok github.com/carlosframework/platform/internal/boxchoice 0.025s
ok github.com/carlosframework/platform/internal/boxid 0.811s
? github.com/carlosframework/platform/internal/boxid/boxidtest [no test files]
? github.com/carlosframework/platform/internal/boxid/internal/signtest [no test files]
ok github.com/carlosframework/platform/internal/boxvolumes 0.019s
ok github.com/carlosframework/platform/internal/caps 0.029s
ok github.com/carlosframework/platform/internal/certwarm 0.008s
ok github.com/carlosframework/platform/internal/cgroup 0.806s
ok github.com/carlosframework/platform/internal/changelog 0.556s
ok github.com/carlosframework/platform/internal/claimactivation 1.232s
ok github.com/carlosframework/platform/internal/clamd 0.068s
? github.com/carlosframework/platform/internal/cliflag [no test files]
? github.com/carlosframework/platform/internal/controlname [no test files]
? github.com/carlosframework/platform/internal/controlsock [no test files]
ok github.com/carlosframework/platform/internal/dbdoor 8.583s
ok github.com/carlosframework/platform/internal/dbdoor/dbdoortest 0.012s
ok github.com/carlosframework/platform/internal/dbquery 0.013s
ok github.com/carlosframework/platform/internal/dbreads 2.171s
? github.com/carlosframework/platform/internal/dbreads/dbreadstest [no test files]
ok github.com/carlosframework/platform/internal/dbverify 0.010s
ok github.com/carlosframework/platform/internal/dnsrec 0.007s
ok github.com/carlosframework/platform/internal/docsite 0.010s
ok github.com/carlosframework/platform/internal/domains 2.987s
ok github.com/carlosframework/platform/internal/doorkey 0.009s
ok github.com/carlosframework/platform/internal/economics 0.010s
ok github.com/carlosframework/platform/internal/edge 45.664s
? github.com/carlosframework/platform/internal/edgeproof [no test files]
ok github.com/carlosframework/platform/internal/edgesfile 0.009s
ok github.com/carlosframework/platform/internal/fleetapi 0.133s
ok github.com/carlosframework/platform/internal/gate 1.654s
ok github.com/carlosframework/platform/internal/genselect 0.016s
ok github.com/carlosframework/platform/internal/geo 0.017s
ok github.com/carlosframework/platform/internal/geoip 0.029s
ok github.com/carlosframework/platform/internal/historyreadback 0.157s
ok github.com/carlosframework/platform/internal/host 146.241s
ok github.com/carlosframework/platform/internal/hostactors 5.672s
ok github.com/carlosframework/platform/internal/hostactors/awsmembership 0.016s
ok github.com/carlosframework/platform/internal/hostbox 2.850s
ok github.com/carlosframework/platform/internal/hostclaim 0.778s
ok github.com/carlosframework/platform/internal/hostenv 0.018s
ok github.com/carlosframework/platform/internal/hostevidence 0.018s
ok github.com/carlosframework/platform/internal/hostexec 5.373s
ok github.com/carlosframework/platform/internal/hostfence 0.040s
ok github.com/carlosframework/platform/internal/hostfiles 10.366s
ok github.com/carlosframework/platform/internal/hostfloor 0.021s
ok github.com/carlosframework/platform/internal/hostguard 1.063s
ok github.com/carlosframework/platform/internal/hostops 1.950s
? github.com/carlosframework/platform/internal/hostops/fencerelease [no test files]
ok github.com/carlosframework/platform/internal/hostops/internal/fencerelease 0.021s
ok github.com/carlosframework/platform/internal/hostquarantine 0.016s
ok github.com/carlosframework/platform/internal/hostrelocate 0.650s
ok github.com/carlosframework/platform/internal/hostresolve 0.161s
ok github.com/carlosframework/platform/internal/hostwrite 0.015s
ok github.com/carlosframework/platform/internal/infraview 0.021s
ok github.com/carlosframework/platform/internal/instancectl 5.226s
ok github.com/carlosframework/platform/internal/instanceexec 2.830s
ok github.com/carlosframework/platform/internal/instancefs 0.028s
ok github.com/carlosframework/platform/internal/instancekey 0.023s
ok github.com/carlosframework/platform/internal/instanceupdates 0.039s
ok github.com/carlosframework/platform/internal/layout 1.782s
ok github.com/carlosframework/platform/internal/lbhealth 0.015s
ok github.com/carlosframework/platform/internal/lease 10.350s
ok github.com/carlosframework/platform/internal/lease/leasetest 0.636s
ok github.com/carlosframework/platform/internal/ledger 0.049s
ok github.com/carlosframework/platform/internal/mail 0.208s
ok github.com/carlosframework/platform/internal/maintenance 0.098s
ok github.com/carlosframework/platform/internal/maintenanceexec 5.573s
ok github.com/carlosframework/platform/internal/maintenanceguard 0.095s
ok github.com/carlosframework/platform/internal/metrics 0.008s
ok github.com/carlosframework/platform/internal/moves 22.585s
ok github.com/carlosframework/platform/internal/moves/archive 0.944s
ok github.com/carlosframework/platform/internal/objstore 0.307s
ok github.com/carlosframework/platform/internal/ops 0.189s
ok github.com/carlosframework/platform/internal/park 0.286s
ok github.com/carlosframework/platform/internal/plans 0.019s
ok github.com/carlosframework/platform/internal/privatefile 0.015s
ok github.com/carlosframework/platform/internal/provision 24.005s
ok github.com/carlosframework/platform/internal/publicrelease 0.009s
? github.com/carlosframework/platform/internal/regionname [no test files]
ok github.com/carlosframework/platform/internal/registry 5.668s
ok github.com/carlosframework/platform/internal/relay 7.231s
ok github.com/carlosframework/platform/internal/release 26.246s
ok github.com/carlosframework/platform/internal/rollout 3.802s
ok github.com/carlosframework/platform/internal/rolloutwords 0.004s
ok github.com/carlosframework/platform/internal/rootaws 0.010s
ok github.com/carlosframework/platform/internal/rootparticipation 0.061s
ok github.com/carlosframework/platform/internal/rootstate 0.028s
ok github.com/carlosframework/platform/internal/runs 0.513s
ok github.com/carlosframework/platform/internal/s3compat 0.005s
ok github.com/carlosframework/platform/internal/safefile 0.004s
ok github.com/carlosframework/platform/internal/schedule 2.669s
ok github.com/carlosframework/platform/internal/seeds 0.011s
ok github.com/carlosframework/platform/internal/sim/engines 2.603s
ok github.com/carlosframework/platform/internal/sim/hostlife 10.879s
ok github.com/carlosframework/platform/internal/sim/litestreamfacts 3.915s
ok github.com/carlosframework/platform/internal/status 0.084s
ok github.com/carlosframework/platform/internal/steering 0.008s
ok github.com/carlosframework/platform/internal/stopevidence 0.294s
ok github.com/carlosframework/platform/internal/svcrole 0.004s
ok github.com/carlosframework/platform/internal/telemetry 0.005s
ok github.com/carlosframework/platform/internal/termfence 0.004s
ok github.com/carlosframework/platform/internal/tunnelclient 0.005s
ok github.com/carlosframework/platform/internal/unixdial 0.004s
ok github.com/carlosframework/platform/internal/updatewords 0.007s
ok github.com/carlosframework/platform/internal/usage 0.005s
ok github.com/carlosframework/platform/internal/version 0.002s
ok github.com/carlosframework/platform/internal/vet 1.694s
=== internal_console@0 (87 s)
ok github.com/carlosframework/platform/internal/console 84.808s
=== internal_console@1 (44 s)
ok github.com/carlosframework/platform/internal/console 41.644s
=== internal_console@2 (42 s)
ok github.com/carlosframework/platform/internal/console 39.901s
=== internal_console@3 (42 s)
ok github.com/carlosframework/platform/internal/console 39.858s
=== internal_console@4 (42 s)
ok github.com/carlosframework/platform/internal/console 40.447s
=== internal_console@5 (41 s)
ok github.com/carlosframework/platform/internal/console 39.298s
=== internal_console@6 (38 s)
ok github.com/carlosframework/platform/internal/console 35.776s
=== internal_console@7 (48 s)
ok github.com/carlosframework/platform/internal/console 45.744s
=== root@0 (106 s)
ok github.com/carlosframework/platform 104.065s
=== root@1 (81 s)
ok github.com/carlosframework/platform 79.145s
=== root@2 (100 s)
ok github.com/carlosframework/platform 97.596s
=== cross ===
--- shard 0 of 5 ---
cross-compiling darwin/arm64
--- shard 1 of 5 ---
cross-compiling darwin/amd64
--- shard 2 of 5 ---
cross-compiling linux/arm64
linux/arm64 carlos binary: 50659490 bytes (ceiling 52000000)
--- shard 3 of 5 ---
cross-compiling windows/amd64
--- shard 4 of 5 ---
cross-compiling windows/arm64
=== minio-contract ===
minio contract: arm64, minio RELEASE.2025-09-07T16-13-09Z, mc RELEASE.2025-08-13T08-35-41Z
minio: downloading https://github.com/minio/minio/releases/download/RELEASE.2025-09-07T16-13-09Z/minio.linux-arm64.RELEASE.2025-09-07T16-13-09Z
minio: downloaded, digest verified
mc: downloading https://github.com/minio/mc/releases/download/RELEASE.2025-08-13T08-35-41Z/mc.linux-arm64.RELEASE.2025-08-13T08-35-41Z
mc: downloaded, digest verified
minio healthy on 127.0.0.1:20999
Added `local` successfully.
Bucket created successfully `local/contract-bucket`.
Bucket created successfully `local/denied-bucket`.
Created policy `contract-only` successfully.
Added user `scopeduser` successfully.
Attached Policies: [contract-only]
To User: scopeduser
buckets and scoped user ready
=== RUN TestDeliveredContractMinio
=== RUN TestDeliveredContractMinio/put
=== RUN TestDeliveredContractMinio/get_round-trips_the_body
=== RUN TestDeliveredContractMinio/list_shows_the_key
=== RUN TestDeliveredContractMinio/head_and_list_agree_on_the_version
=== RUN TestDeliveredContractMinio/the_app_cannot_tag_its_own_object
=== RUN TestDeliveredContractMinio/the_app_cannot_tag_its_own_object/not_by_PutObjectTagging
=== RUN TestDeliveredContractMinio/the_app_cannot_tag_its_own_object/and_not_by_x-amz-tagging_on_upload
=== RUN TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route
=== RUN TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/not_by_CreateMultipartUpload
=== RUN TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/not_by_CopyObject_with_a_REPLACE_tagging_directive
=== RUN TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/but_multipart_itself_is_allowed
=== RUN TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/and_copy_itself_is_allowed
=== RUN TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/a_POST-policy_forged_tag_with_a_matching_version_is_still_not_a_verdict
=== RUN TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/and_an_ordinary_tagged-with-something-else_upload_is_allowed
=== RUN TestDeliveredContractMinio/delete_removes_it
=== RUN TestDeliveredContractMinio/cross-bucket_denial
--- PASS: TestDeliveredContractMinio (0.02s)
--- PASS: TestDeliveredContractMinio/put (0.00s)
--- PASS: TestDeliveredContractMinio/get_round-trips_the_body (0.00s)
--- PASS: TestDeliveredContractMinio/list_shows_the_key (0.00s)
--- PASS: TestDeliveredContractMinio/head_and_list_agree_on_the_version (0.00s)
--- PASS: TestDeliveredContractMinio/the_app_cannot_tag_its_own_object (0.00s)
--- PASS: TestDeliveredContractMinio/the_app_cannot_tag_its_own_object/not_by_PutObjectTagging (0.00s)
--- PASS: TestDeliveredContractMinio/the_app_cannot_tag_its_own_object/and_not_by_x-amz-tagging_on_upload (0.00s)
--- PASS: TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route (0.01s)
--- PASS: TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/not_by_CreateMultipartUpload (0.00s)
--- PASS: TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/not_by_CopyObject_with_a_REPLACE_tagging_directive (0.00s)
--- PASS: TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/but_multipart_itself_is_allowed (0.00s)
--- PASS: TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/and_copy_itself_is_allowed (0.00s)
--- PASS: TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/a_POST-policy_forged_tag_with_a_matching_version_is_still_not_a_verdict (0.00s)
--- PASS: TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/and_an_ordinary_tagged-with-something-else_upload_is_allowed (0.00s)
--- PASS: TestDeliveredContractMinio/delete_removes_it (0.00s)
--- PASS: TestDeliveredContractMinio/cross-bucket_denial (0.00s)
PASS
ok github.com/carlosframework/platform/internal/objstore 0.024s
=== RUN TestT4StorageProbeMinio
2026/10/24 20:17:51 moves: T4: STORE: list: operation error S3: ListObjectsV2, https response error StatusCode: 403, RequestID: 18E1908F401B0E0B, HostID: 7c420053e0fbcd7f41653fdd92ffa2802c903d05303b1aa15adc2e8bb65d183f, api error SignatureDoesNotMatch: The request signature we calculated does not match the signature you provided. Check your key and signing method.
--- PASS: TestT4StorageProbeMinio (0.01s)
PASS
ok github.com/carlosframework/platform/internal/host 0.022s
=== RUN TestS3StoreMinioContract
=== RUN TestS3StoreMinioContract/conditional_get_and_etag_listing
=== RUN TestS3StoreMinioContract/bump_through_every_mutation_path
--- PASS: TestS3StoreMinioContract (0.03s)
--- PASS: TestS3StoreMinioContract/conditional_get_and_etag_listing (0.01s)
--- PASS: TestS3StoreMinioContract/bump_through_every_mutation_path (0.02s)
PASS
ok github.com/carlosframework/platform/internal/release 0.030s
Bucket created successfully `local/dbf-deploy`.
Bucket created successfully `local/dbf-data-ie`.
Bucket created successfully `local/dbf-data-uk`.
local/dbf-deploy versioning is enabled
Created policy `dbf-console` successfully.
Created policy `dbf-ukbox` successfully.
Added user `dbfconsole` successfully.
Added user `dbfukbox` successfully.
Attached Policies: [dbf-console]
To User: dbfconsole
Attached Policies: [dbf-ukbox]
To User: dbfukbox
=== RUN TestDBForwardMinio
2026/10/24 20:17:53 INFO replica sync system=store db=source.db replica=file txid.replica=0000000000000000 txid.db=0000000000000001
2026/10/24 20:17:53 INFO ltx file uploaded system=store db=source.db replica=file level=0 minTXID=0000000000000001 maxTXID=0000000000000001 size=720
2026/10/24 20:17:53 INFO replica sync system=store db=source.db replica=file txid.replica=0000000000000001 txid.db=0000000000000001
2026/10/24 20:17:53 INFO replica sync system=store db=source.db replica=file txid.replica=0000000000000001 txid.db=0000000000000002
2026/10/24 20:17:53 INFO ltx file uploaded system=store db=source.db replica=file level=0 minTXID=0000000000000002 maxTXID=0000000000000002 size=370
2026/10/24 20:17:53 INFO replica sync system=store db=source.db replica=file txid.replica=0000000000000002 txid.db=0000000000000002
2026/10/24 20:17:53 INFO replica sync system=store db=source.db replica=file txid.replica=0000000000000002 txid.db=0000000000000003
2026/10/24 20:17:53 INFO ltx file uploaded system=store db=source.db replica=file level=0 minTXID=0000000000000003 maxTXID=0000000000000003 size=454
2026/10/24 20:17:53 INFO replica sync system=store db=source.db replica=file txid.replica=0000000000000003 txid.db=0000000000000003
2026/10/24 20:17:53 INFO replica sync system=store db=source.db replica=file txid.replica=0000000000000000 txid.db=0000000000000001
2026/10/24 20:17:53 INFO ltx file uploaded system=store db=source.db replica=file level=0 minTXID=0000000000000001 maxTXID=0000000000000001 size=720
2026/10/24 20:17:53 INFO replica sync system=store db=source.db replica=file txid.replica=0000000000000001 txid.db=0000000000000001
2026/10/24 20:17:54 console: CARLOS_SECRETS_PUBKEY is unset, so no region outside the home can be chosen automatically
2026/10/24 20:17:54 console: could not notify member@example.com of a CLI token issue: keymail: lookup member@example.com: keymail: GET /api/federation/lookup?addr=member%40example.com&from=console%40127.0.0.1%3A33519: could not determine target mailbox
agent.go:135: dbreads: dbr_cfonhhj4k6hw3mssjemmykjoxi: planned 3 files (1544 bytes) and restored to 3 in 9ms
agent.go:135: dbreads: dbr_cfonhhj4k6hw3mssjemmykjoxi: output 5 bytes in 1 confirmed chunks in 1ms
2026/10/24 20:17:55 console: db read dbr_cfonhhj4k6hw3mssjemmykjoxi: relayed 1 chunks (102 bytes) in 1.004s, ended done
agent.go:135: dbreads: dbr_7q6b6ptnbnlwj2buoatlsvg6vq: planned 1 files (720 bytes) and restored to 1 in 8ms
agent.go:135: dbreads: dbr_7q6b6ptnbnlwj2buoatlsvg6vq: output 5 bytes in 1 confirmed chunks in 1ms
2026/10/24 20:17:56 console: db read dbr_7q6b6ptnbnlwj2buoatlsvg6vq: relayed 1 chunks (102 bytes) in 1.003s, ended done
agent.go:135: dbreads: dbr_bdvteg2nyqnaqxx4zfskhspav4: planned 3 files (1544 bytes) and held through 3 in 9ms
agent.go:135: dbreads: dbr_bdvteg2nyqnaqxx4zfskhspav4: output 1544 bytes in 1 confirmed chunks in 1ms
2026/10/24 20:17:57 console: db read dbr_bdvteg2nyqnaqxx4zfskhspav4: relayed 1 chunks (1641 bytes) in 1.003s, ended done
=== RUN TestDBForwardMinio/by_disconnect
=== NAME TestDBForwardMinio
agent.go:135: dbreads: dbr_l2zai6kjn66gntwzjnqpksvr5i: planned 3 files (1544 bytes) and restored to 3 in 9ms
2026/10/24 20:17:57 console: db read dbr_l2zai6kjn66gntwzjnqpksvr5i: relayed 0 chunks (0 bytes) in 13ms, ended client gone
agent.go:135: dbreads: dbr_l2zai6kjn66gntwzjnqpksvr5i: output 5 bytes in 1 confirmed chunks in 32ms
=== RUN TestDBForwardMinio/by_timeout
=== NAME TestDBForwardMinio
agent.go:135: dbreads: dbr_owitieekwold5erqunqn62z4za: planned 3 files (1544 bytes) and restored to 3 in 9ms
2026/10/24 20:18:17 console: db read dbr_owitieekwold5erqunqn62z4za: relayed 0 chunks (0 bytes) in 19.786s, ended no-result
agent.go:135: dbreads: dbr_owitieekwold5erqunqn62z4za: output 5 bytes in 1 confirmed chunks in 19.776s
--- PASS: TestDBForwardMinio (23.14s)
--- PASS: TestDBForwardMinio/by_disconnect (0.15s)
--- PASS: TestDBForwardMinio/by_timeout (19.89s)
PASS
ok github.com/carlosframework/platform/internal/console 23.156s
minio contract: PASS
diff --git a/approved_moves_copy_test.go b/approved_moves_copy_test.go| index 7d15e7bba..812fb7b02 100644 |
| --- a/approved_moves_copy_test.go |
| +++ b/approved_moves_copy_test.go |
| @@ -10,6 +10,7 @@ import ( |
| "os/exec" |
| "path/filepath" |
| "regexp" |
| + "slices" |
| "sort" |
| "strconv" |
| "strings" |
| @@ -145,8 +146,14 @@ func parseMovesCopyTable(src, name string) ([]copyRow, error) { |
| // parseCopyTable reads a copy record's tables: the moves record, the run |
| // deck and the seeded deck share the format. ids is the record's id pattern |
| -// and idKind names it in the refusal. |
| +// and idKind names it in the refusal. A row's status is pending or approved. |
| func parseCopyTable(src, name string, ids *regexp.Regexp, idKind string) ([]copyRow, error) { |
| + return parseCopyTableStatuses(src, name, ids, idKind, "pending", "approved") |
| +} |
| + |
| +// parseCopyTableStatuses is parseCopyTable for a deck with its own statuses |
| +// (the rootfs deck adds struck). |
| +func parseCopyTableStatuses(src, name string, ids *regexp.Regexp, idKind string, statuses ...string) ([]copyRow, error) { |
| var rows []copyRow |
| seen := map[string]int{} |
| inTable := false |
| @@ -194,8 +201,11 @@ func parseCopyTable(src, name string, ids *regexp.Regexp, idKind string) ([]copy |
| if r.File == "" || r.Text == "" { |
| return nil, fmt.Errorf("%s: %s has an empty file or text cell", at, r.ID) |
| } |
| - if r.Status != "pending" && r.Status != "approved" { |
| - return nil, fmt.Errorf("%s: %s has status %q, which is neither pending nor approved", at, r.ID, r.Status) |
| + if !slices.Contains(statuses, r.Status) { |
| + if slices.Equal(statuses, []string{"pending", "approved"}) { |
| + return nil, fmt.Errorf("%s: %s has status %q, which is neither pending nor approved", at, r.ID, r.Status) |
| + } |
| + return nil, fmt.Errorf("%s: %s has status %q, which is not one of %s", at, r.ID, r.Status, strings.Join(statuses, ", ")) |
| } |
| if first, dup := seen[r.ID]; dup { |
| return nil, fmt.Errorf("%s: %s is already a row at line %d", at, r.ID, first) |
diff --git a/approved_rootfs_copy_test.go b/approved_rootfs_copy_test.go| new file mode 100644 |
| index 000000000..a8810501e |
| --- /dev/null |
| +++ b/approved_rootfs_copy_test.go |
| @@ -0,0 +1,242 @@ |
| +// approved_rootfs_copy_test.go |
| +package main |
| + |
| +import ( |
| + "os" |
| + "regexp" |
| + "slices" |
| + "strings" |
| + "testing" |
| +) |
| + |
| +// The copy gate of the rootfs apps series (Plan 1 Task 1), over the one deck |
| +// every rootfs plan's string table was assembled into: |
| +// |
| +// - TestRootfsCopyRecordIsComplete: the deck names exactly rootfsCopyIDs; |
| +// - TestRootfsCopyHasNoEmDash; |
| +// - TestRootfsBuiltCopyIsApproved: an id in rootfsCopyBuilt is an approved |
| +// row, so no pending string reaches code or docs; |
| +// - TestRootfsBuiltCopyIsApplied: a built row's text is in the file it |
| +// names. |
| +const rootfsCopyRecordPath = "docs/superpowers/plans/2026-10-24-rootfs-apps-copy.md" |
| + |
| +// Canonical ids only: two digits from C01 to C99, three digits from C100 |
| +// (Plan 4 runs to RFS-4-C138). No leading zero on a three-digit id, no C00. |
| +var rootfsCopyIDPattern = regexp.MustCompile(`^RFS-[1-6]-C(0[1-9]|[1-9][0-9]{1,2})$`) |
| + |
| +// rootfsCopyIDs is the deck's inventory, pinned as a literal list in the |
| +// deck's order: RFS-1-C01 to RFS-1-C85, then each later plan's rows. A |
| +// struck row stays in it: the deck keeps the row for history. |
| +var rootfsCopyIDs = []string{ |
| + // Plan 1 |
| + "RFS-1-C01", "RFS-1-C02", "RFS-1-C03", "RFS-1-C04", |
| + "RFS-1-C05", "RFS-1-C06", "RFS-1-C07", "RFS-1-C08", |
| + "RFS-1-C09", "RFS-1-C10", "RFS-1-C11", "RFS-1-C12", |
| + "RFS-1-C13", "RFS-1-C14", "RFS-1-C15", "RFS-1-C16", |
| + "RFS-1-C17", "RFS-1-C18", "RFS-1-C19", "RFS-1-C20", |
| + "RFS-1-C21", "RFS-1-C22", "RFS-1-C23", "RFS-1-C24", |
| + "RFS-1-C25", "RFS-1-C26", "RFS-1-C27", "RFS-1-C28", |
| + "RFS-1-C29", "RFS-1-C30", "RFS-1-C31", "RFS-1-C32", |
| + "RFS-1-C33", "RFS-1-C34", "RFS-1-C35", "RFS-1-C36", |
| + "RFS-1-C37", "RFS-1-C38", "RFS-1-C39", "RFS-1-C40", |
| + "RFS-1-C41", "RFS-1-C42", "RFS-1-C43", "RFS-1-C44", |
| + "RFS-1-C45", "RFS-1-C46", "RFS-1-C47", "RFS-1-C48", |
| + "RFS-1-C49", "RFS-1-C50", "RFS-1-C51", "RFS-1-C52", |
| + "RFS-1-C53", "RFS-1-C54", "RFS-1-C55", "RFS-1-C56", |
| + "RFS-1-C57", "RFS-1-C58", "RFS-1-C59", "RFS-1-C60", |
| + "RFS-1-C61", "RFS-1-C62", "RFS-1-C63", "RFS-1-C64", |
| + "RFS-1-C65", "RFS-1-C66", "RFS-1-C67", "RFS-1-C68", |
| + "RFS-1-C69", "RFS-1-C70", "RFS-1-C71", "RFS-1-C72", |
| + "RFS-1-C73", "RFS-1-C74", "RFS-1-C75", "RFS-1-C76", |
| + "RFS-1-C77", "RFS-1-C78", "RFS-1-C79", "RFS-1-C80", |
| + "RFS-1-C81", "RFS-1-C82", "RFS-1-C83", "RFS-1-C84", |
| + "RFS-1-C85", |
| + // Plan 2 |
| + "RFS-2-C01", "RFS-2-C02", "RFS-2-C03", "RFS-2-C04", |
| + "RFS-2-C05", "RFS-2-C06", "RFS-2-C07", "RFS-2-C08", |
| + "RFS-2-C09", "RFS-2-C10", "RFS-2-C11", "RFS-2-C12", |
| + // Plan 3 |
| + "RFS-3-C01", "RFS-3-C02", "RFS-3-C03", "RFS-3-C04", |
| + "RFS-3-C05", "RFS-3-C06", "RFS-3-C07", "RFS-3-C08", |
| + "RFS-3-C09", "RFS-3-C10", "RFS-3-C11", "RFS-3-C12", |
| + "RFS-3-C13", "RFS-3-C14", "RFS-3-C15", "RFS-3-C16", |
| + "RFS-3-C17", "RFS-3-C18", "RFS-3-C19", "RFS-3-C20", |
| + "RFS-3-C21", "RFS-3-C22", "RFS-3-C23", "RFS-3-C24", |
| + "RFS-3-C25", "RFS-3-C26", "RFS-3-C27", "RFS-3-C28", |
| + "RFS-3-C29", |
| + // Plan 4 |
| + "RFS-4-C01", "RFS-4-C02", "RFS-4-C03", "RFS-4-C04", |
| + "RFS-4-C05", "RFS-4-C06", "RFS-4-C07", "RFS-4-C08", |
| + "RFS-4-C09", "RFS-4-C10", "RFS-4-C11", "RFS-4-C12", |
| + "RFS-4-C13", "RFS-4-C14", "RFS-4-C15", "RFS-4-C16", |
| + "RFS-4-C17", "RFS-4-C18", "RFS-4-C19", "RFS-4-C20", |
| + "RFS-4-C21", "RFS-4-C22", "RFS-4-C23", "RFS-4-C24", |
| + "RFS-4-C25", "RFS-4-C26", "RFS-4-C27", "RFS-4-C28", |
| + "RFS-4-C29", "RFS-4-C30", "RFS-4-C31", "RFS-4-C32", |
| + "RFS-4-C33", "RFS-4-C34", "RFS-4-C35", "RFS-4-C36", |
| + "RFS-4-C37", "RFS-4-C38", "RFS-4-C39", "RFS-4-C40", |
| + "RFS-4-C41", "RFS-4-C42", "RFS-4-C43", "RFS-4-C44", |
| + "RFS-4-C45", "RFS-4-C46", "RFS-4-C47", "RFS-4-C48", |
| + "RFS-4-C49", "RFS-4-C50", "RFS-4-C51", "RFS-4-C52", |
| + "RFS-4-C53", "RFS-4-C54", "RFS-4-C55", "RFS-4-C56", |
| + "RFS-4-C57", "RFS-4-C58", "RFS-4-C59", "RFS-4-C60", |
| + "RFS-4-C61", "RFS-4-C62", "RFS-4-C63", "RFS-4-C64", |
| + "RFS-4-C65", "RFS-4-C66", "RFS-4-C67", "RFS-4-C68", |
| + "RFS-4-C69", "RFS-4-C70", "RFS-4-C71", "RFS-4-C72", |
| + "RFS-4-C73", "RFS-4-C74", "RFS-4-C75", "RFS-4-C76", |
| + "RFS-4-C77", "RFS-4-C78", "RFS-4-C79", "RFS-4-C80", |
| + "RFS-4-C81", "RFS-4-C82", "RFS-4-C83", "RFS-4-C84", |
| + "RFS-4-C85", "RFS-4-C86", "RFS-4-C87", "RFS-4-C88", |
| + "RFS-4-C89", "RFS-4-C90", "RFS-4-C91", "RFS-4-C92", |
| + "RFS-4-C93", "RFS-4-C94", "RFS-4-C95", "RFS-4-C96", |
| + "RFS-4-C97", "RFS-4-C98", "RFS-4-C99", "RFS-4-C100", |
| + "RFS-4-C101", "RFS-4-C102", "RFS-4-C103", "RFS-4-C104", |
| + "RFS-4-C105", "RFS-4-C106", "RFS-4-C107", "RFS-4-C108", |
| + "RFS-4-C109", "RFS-4-C110", "RFS-4-C111", "RFS-4-C112", |
| + "RFS-4-C113", "RFS-4-C114", "RFS-4-C115", "RFS-4-C116", |
| + "RFS-4-C117", "RFS-4-C118", "RFS-4-C119", "RFS-4-C120", |
| + "RFS-4-C121", "RFS-4-C122", "RFS-4-C123", "RFS-4-C124", |
| + "RFS-4-C125", "RFS-4-C126", "RFS-4-C127", "RFS-4-C128", |
| + "RFS-4-C129", "RFS-4-C130", "RFS-4-C131", "RFS-4-C132", |
| + "RFS-4-C133", "RFS-4-C134", "RFS-4-C135", "RFS-4-C136", |
| + "RFS-4-C137", "RFS-4-C138", |
| + // Plan 5 |
| + "RFS-5-C01", "RFS-5-C02", "RFS-5-C03", "RFS-5-C04", |
| + "RFS-5-C05", "RFS-5-C06", "RFS-5-C07", "RFS-5-C08", |
| + "RFS-5-C09", "RFS-5-C10", "RFS-5-C11", "RFS-5-C12", |
| + "RFS-5-C13", "RFS-5-C14", "RFS-5-C15", "RFS-5-C16", |
| + "RFS-5-C17", "RFS-5-C18", "RFS-5-C19", "RFS-5-C20", |
| + "RFS-5-C21", "RFS-5-C22", "RFS-5-C23", "RFS-5-C24", |
| + "RFS-5-C25", "RFS-5-C26", "RFS-5-C27", "RFS-5-C28", |
| + "RFS-5-C29", "RFS-5-C30", "RFS-5-C31", "RFS-5-C32", |
| + "RFS-5-C33", "RFS-5-C34", "RFS-5-C35", "RFS-5-C36", |
| + "RFS-5-C37", "RFS-5-C38", "RFS-5-C39", "RFS-5-C40", |
| + "RFS-5-C41", "RFS-5-C42", "RFS-5-C43", "RFS-5-C44", |
| + // Plan 6 |
| + "RFS-6-C01", "RFS-6-C02", "RFS-6-C03", "RFS-6-C04", |
| + "RFS-6-C05", "RFS-6-C06", "RFS-6-C07", "RFS-6-C08", |
| + "RFS-6-C09", "RFS-6-C10", "RFS-6-C11", "RFS-6-C12", |
| + "RFS-6-C13", "RFS-6-C14", "RFS-6-C15", "RFS-6-C16", |
| + "RFS-6-C17", "RFS-6-C18", "RFS-6-C19", "RFS-6-C20", |
| + "RFS-6-C21", "RFS-6-C22", "RFS-6-C23", "RFS-6-C24", |
| + "RFS-6-C25", "RFS-6-C26", "RFS-6-C27", "RFS-6-C28", |
| + "RFS-6-C29", "RFS-6-C30", "RFS-6-C31", "RFS-6-C32", |
| + "RFS-6-C33", "RFS-6-C34", "RFS-6-C35", "RFS-6-C36", |
| + "RFS-6-C37", "RFS-6-C38", "RFS-6-C39", "RFS-6-C40", |
| + "RFS-6-C41", "RFS-6-C42", "RFS-6-C43", "RFS-6-C44", |
| + "RFS-6-C45", "RFS-6-C46", "RFS-6-C47", "RFS-6-C48", |
| + "RFS-6-C49", "RFS-6-C50", "RFS-6-C51", "RFS-6-C52", |
| + "RFS-6-C53", "RFS-6-C54", "RFS-6-C55", "RFS-6-C56", |
| + "RFS-6-C57", "RFS-6-C58", "RFS-6-C59", "RFS-6-C60", |
| + "RFS-6-C61", "RFS-6-C62", "RFS-6-C63", "RFS-6-C64", |
| +} |
| + |
| +// rootfsCopyStruck is every id the deck marks struck, pinned, so a row can |
| +// neither become struck nor stop being struck without this list changing in |
| +// the same commit. Empty at Task 1: RFS-1-C15 and RFS-3-C28 are live until |
| +// Plan 5 Task 2a and Plan 6 Task 16 strike them. |
| +var rootfsCopyStruck = []string{} |
| + |
| +// rootfsCopyStatuses are the statuses the deck may carry. struck is the |
| +// rootfs deck's own: a struck row is never reviewed, never built and never |
| +// checked as applied. The older decks keep parseCopyTable's two statuses. |
| +var rootfsCopyStatuses = []string{"pending", "approved", "struck"} |
| + |
| +// rootfsCopyBuilt is every id whose text the code or docs carry. The task |
| +// that writes a row's text adds its id here in the same commit, once Paul |
| +// has approved the row. |
| +var rootfsCopyBuilt = []string{} |
| + |
| +func mustParseRootfsRecord(t *testing.T) []copyRow { |
| + t.Helper() |
| + raw, err := os.ReadFile(rootfsCopyRecordPath) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + rows, err := parseCopyTableStatuses(string(raw), rootfsCopyRecordPath, rootfsCopyIDPattern, "an RFS-<plan>-C<nn>", rootfsCopyStatuses...) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + return rows |
| +} |
| + |
| +func TestRootfsCopyIDPattern(t *testing.T) { |
| + for _, ok := range []string{"RFS-1-C01", "RFS-6-C99", "RFS-4-C100", "RFS-4-C138"} { |
| + if !rootfsCopyIDPattern.MatchString(ok) { |
| + t.Errorf("%s refused", ok) |
| + } |
| + } |
| + for _, bad := range []string{"RFS-1-C1", "RFS-7-C01", "RFS-0-C01", "RFS-1-C00", "RFS-1-C001", "RFS-4-C0100", "RFS-4-C1000", "RUN-C1", "rfs-1-c01"} { |
| + if rootfsCopyIDPattern.MatchString(bad) { |
| + t.Errorf("%s accepted", bad) |
| + } |
| + } |
| +} |
| + |
| +// The real deck, not a fixture (Astra R2 finding 5): it parses under the |
| +// rootfs statuses, holds three-digit ids, and its struck rows are exactly |
| +// rootfsCopyStruck. |
| +func TestRootfsCopyDeckParses(t *testing.T) { |
| + rows := mustParseRootfsRecord(t) |
| + if len(rows) != len(rootfsCopyIDs) { |
| + t.Fatalf("the deck has %d rows, the inventory %d", len(rows), len(rootfsCopyIDs)) |
| + } |
| + var three bool |
| + var struck []string |
| + for _, r := range rows { |
| + three = three || r.ID == "RFS-4-C100" |
| + if r.Status == "struck" { |
| + struck = append(struck, r.ID) |
| + } |
| + } |
| + if !three { |
| + t.Error("the deck has no RFS-4-C100: three-digit ids did not parse") |
| + } |
| + if !slices.Equal(struck, rootfsCopyStruck) { |
| + t.Errorf("struck rows %v, pinned %v", struck, rootfsCopyStruck) |
| + } |
| + // The older decks' parser still refuses the rootfs-only status. |
| + src := "| id | file | text | status |\n|---|---|---|---|\n| RFS-1-C01 | a.go | one | struck |\n" |
| + if _, err := parseCopyTable(src, "x.md", rootfsCopyIDPattern, "an RFS-<plan>-C<nn>"); err == nil || !strings.Contains(err.Error(), "neither pending nor approved") { |
| + t.Errorf("parseCopyTable accepted struck: %v", err) |
| + } |
| + // The variadic form names its statuses in the refusal. |
| + src = "| id | file | text | status |\n|---|---|---|---|\n| RFS-1-C01 | a.go | one | maybe |\n" |
| + if _, err := parseCopyTableStatuses(src, "x.md", rootfsCopyIDPattern, "an RFS-<plan>-C<nn>", rootfsCopyStatuses...); err == nil || !strings.Contains(err.Error(), "not one of pending, approved, struck") { |
| + t.Errorf("parseCopyTableStatuses accepted maybe: %v", err) |
| + } |
| +} |
| + |
| +func TestRootfsCopyRecordIsComplete(t *testing.T) { |
| + reportAll(t, checkMovesInventory(mustParseRootfsRecord(t), rootfsCopyIDs)) |
| +} |
| + |
| +func TestRootfsCopyHasNoEmDash(t *testing.T) { |
| + reportAll(t, checkMovesNoEmDash(mustParseRootfsRecord(t))) |
| +} |
| + |
| +func TestRootfsBuiltCopyIsApproved(t *testing.T) { |
| + reportAll(t, checkRunBuilt(mustParseRootfsRecord(t), rootfsCopyBuilt)) |
| +} |
| + |
| +// rootfsAppliedText is a row's text as the code prints it: the deck writes a |
| +// newline as a literal backslash-n, and Markdown needs a backslash before a |
| +// pipe inside a table cell (RFS-1-C34, RFS-5-C36, RFS-6-C41), which Go must |
| +// not print. |
| +func rootfsAppliedText(s string) string { |
| + return strings.NewReplacer(`\n`, "\n", `\|`, "|").Replace(s) |
| +} |
| + |
| +func TestRootfsAppliedTextUnescapes(t *testing.T) { |
| + if got, want := rootfsAppliedText(`a \| b\nc`), "a | b\nc"; got != want { |
| + t.Errorf("rootfsAppliedText = %q, want %q", got, want) |
| + } |
| +} |
| + |
| +func TestRootfsBuiltCopyIsApplied(t *testing.T) { |
| + rows := builtRows(mustParseRootfsRecord(t), rootfsCopyBuilt) |
| + for i := range rows { |
| + rows[i].Text = rootfsAppliedText(rows[i].Text) |
| + } |
| + reportAll(t, checkRunApplied(rows, ".")) |
| +} |