diff --git a/corpus/accepted.json b/corpus/accepted.json index 62690172..94af900c 100644 --- a/corpus/accepted.json +++ b/corpus/accepted.json @@ -19,7 +19,7 @@ "made their deletion compulsory the day the emulator stopped producing them.", "The list then sat empty for one reason nobody had written down: the corpus", "held nothing billed. A VPC, a private network and an SSH key are free, so", - "the two recordings covered exactly the operations that cost nothing \u2014 and", + "the two recordings covered exactly the operations that cost nothing — and", "every ReplayInvariant this repository declares lives on CreateServer,", "GetServer and UpdateServer. values_checked was 0 and orders_checked was 0,", "including for the order of Server.public_ips, which is #320, a defect that", @@ -34,8 +34,8 @@ "surfaced the twenty-six divergences below.", "", "THE FOUR EXOSCALE ENTRIES ARE GONE, AND THAT IS THE RULE ABOVE DOING ITS", - "WORK. The #354 recording surfaced four divergences of one kind \u2014 a field the", - "cloud answers that this emulator omitted \u2014 and getting to see them at all", + "WORK. The #354 recording surfaced four divergences of one kind — a field the", + "cloud answers that this emulator omitted — and getting to see them at all", "took fixing the instrument first: three sanitiser defects buried them, all", "measured on the same file and all falsified in", "tools/falsify/specs/sanitised-corpus.json:", @@ -52,9 +52,9 @@ " below start-ip`. Together those two hid the whole private-network", " lifecycle behind about twenty findings, not one of them the emulator's.", "", - "What was left after that was three fields across four entries \u2014 zones[].id", + "What was left after that was three fields across four entries — zones[].id", "(51 findings), security-groups[].visibility on the list (44) and on the get", - "(2), and rules[].security-group.name (8) \u2014 and #370 and #371 retired all", + "(2), and rules[].security-group.name (8) — and #370 and #371 retired all", "four. Their shared root is worth keeping written down, because it is the one", "no document-reading control could ever have found: the cloud answers fields", "its own published API description does not declare, so the contract, the", @@ -62,7 +62,7 @@ "were wrong the same way. Two of the three now live in", "tools/contract/exoscale-recorded-fields.yaml, which folds a field into the", "contract only with the recording that proves it and is held to that recording", - "by internal/cli's TestEveryRecordedFieldIsStillOnTheWire \u2014 the same staleness", + "by internal/cli's TestEveryRecordedFieldIsStillOnTheWire — the same staleness", "rule as this list, pointed the other way: a citation that cites nothing is as", "dead as an exemption that excuses nothing. The third needed no contract", "change at all; the document already declared the field and only the pack had", @@ -70,7 +70,7 @@ "", "An empty list is not a licence. The next divergence a recording surfaces", "goes here with its reason and the issue that retires it, or the gate stays", - "red \u2014 and a corpus trimmed until it passes is the one outcome this file", + "red — and a corpus trimmed until it passes is the one outcome this file", "exists to make impossible.", "THEY ARE CLASSIFIED AND NOT FIXED, DELIBERATELY. Classifying a divergence", "and correcting it are two pieces of work, and doing them in one pass is how", @@ -82,7 +82,7 @@ " read it through block/v1alpha1; this emulator gives a local instance/v1", " volume, so the read and the delete answer 404. Sixteen entries, and the", " fourteen absent fields are the body of an object that is simply not", - " there \u2014 one wildcard entry rather than fourteen, because they have one", + " there — one wildcard entry rather than fourteen, because they have one", " cause and would be retired by one change.", " - #366, two keys the cloud writes and this emulator omits: bootscript", " (null) and extra_networks ([]). Untriaged, which is the point of the", @@ -94,7 +94,7 @@ " content is a constant that recording states is not a field this", " emulator cannot answer -- which is what DeclinedFields() is for.", " - #367, image.from_server is an empty string on the wire and null here.", - " FIXED ON 2026-08-27, and its seven entries are gone \u2014 the catalogue", + " FIXED ON 2026-08-27, and its seven entries are gone — the catalogue", " image view now types the field the way the SDK declares it (a value,", " not a pointer) and the way both recordings carry it, which is also the", " way this pack's OWN clientImageView had always answered it.", @@ -122,8 +122,8 @@ "credentials of tools/conformance/outscale/fake-credentials.env, in a config", "file holding exactly one profile named on the command line. Measured on", "2026-08-21 against api.eu-west-2.outscale.com: five operations answer 200", - "to an unknown access key \u2014 ReadRegions, ReadVmTypes, ReadPublicIpRanges,", - "ReadPublicCatalog, ReadFlexibleGpuCatalog \u2014 and every authenticated one", + "to an unknown access key — ReadRegions, ReadVmTypes, ReadPublicIpRanges,", + "ReadPublicCatalog, ReadFlexibleGpuCatalog — and every authenticated one", "answers 400 InvalidParameterValue 4120. So the provider's own catalogue is", "recordable by anyone, from any station, with no account to put at risk and", "no inventory in the answers, and this is the first time this repository has", @@ -133,7 +133,7 @@ "OUTSCALE ACCOUNT (#354), and the same sentence applies to them as to the", "Scaleway ones: they are classified and not fixed, deliberately. That", "recording is also the reason this file can say anything about an Outscale", - "value or an Outscale order at all \u2014 the pack declared no ReplayInvariant", + "value or an Outscale order at all — the pack declared no ReplayInvariant", "until #354, so `feint corpus --check` had been printing \"0 divergent", "finding(s)\" over a run in which no value and no order of an Outscale answer", "was ever compared. values_checked and orders_checked went from 2 and 6, all", @@ -151,7 +151,7 @@ " db-then-web, so the invariant holds the emulator to the cloud's order.", " - #380, a delete is instantaneous here and asynchronous upstream, so every", " read of the teardown answers a terminal-state object there and nothing", - " here \u2014 and a security group a just-terminated machine still holds is", + " here — and a security group a just-terminated machine still holds is", " refused 409 for two minutes upstream and accepted at once here.", " - #381, DeleteRoute and DeleteLoadBalancer answer no body.", " - #382, a rule whose source is another group omits that group's AccountId.", @@ -175,7 +175,7 @@ "The first three are fixed and falsified in tools/falsify/specs/.", "", "HOW A CORPUS AGES. `warn_after_days` warns and never fails. This gate holds", - "one side of the comparison \u2014 it can say the emulator and the recording", + "one side of the comparison — it can say the emulator and the recording", "disagree, and nothing in it knows which of the two moved. Failing on age", "would be asserting exactly what it cannot measure, and a gate that fails", "because the cloud changed is a gate somebody disables. #359 is the half that", @@ -278,11 +278,147 @@ ], "accepted": [ { - "file": "outscale/oapi-cli-lifecycle.jsonl", + "file": "exoscale/exo-refusals.jsonl", + "operation": "exoscale/v2.create-private-network", + "kind": "absent", + "path": "errors", + "reason": "The status and the refusal are right and the body is short one key: the cloud carries an errors array naming the field it refused, this emulator carries the message alone. Adding the array unconditionally would put an empty one on the refusals the cloud answers without it, so a fix waits for a recording that shows both shapes.", + "issue": "https://github.com/stephrobert/feint/issues/397" + }, + { + "file": "outscale/oapi-cli-lb-shapes.jsonl", + "operation": "osc/Client.DeleteLoadBalancer", + "kind": "absent", + "path": "LoadBalancer", + "reason": "A delete is instantaneous here and asynchronous upstream, so the two disagree on every read of a teardown. MEASURED AGAIN on 2026-08-24 and at much greater length than #354 could (#427): this recording's teardown retries until a read proves the object gone, so it carries 26 DeleteLoadBalancer attempts and 24 DeleteNet ones, each answered by the cloud with a 400 or a 409 while the object settled and by this emulator with a 200 at the first ask. The 'absent' findings on the same operations are the same cause seen from the body: an Errors array where the cloud refused, or an empty list where the cloud still held the object. Nothing here is a field the pack fails to serve.", + "issue": "https://github.com/stephrobert/feint/issues/380" + }, + { + "file": "outscale/oapi-cli-lb-shapes.jsonl", + "operation": "osc/Client.DeleteLoadBalancer", + "kind": "absent", + "path": "LoadBalancer.HealthCheck.Path", + "reason": "A delete is instantaneous here and asynchronous upstream, so the two disagree on every read of a teardown. MEASURED AGAIN on 2026-08-24 and at much greater length than #354 could (#427): this recording's teardown retries until a read proves the object gone, so it carries 26 DeleteLoadBalancer attempts and 24 DeleteNet ones, each answered by the cloud with a 400 or a 409 while the object settled and by this emulator with a 200 at the first ask. The 'absent' findings on the same operations are the same cause seen from the body: an Errors array where the cloud refused, or an empty list where the cloud still held the object. Nothing here is a field the pack fails to serve.", + "issue": "https://github.com/stephrobert/feint/issues/380" + }, + { + "file": "outscale/oapi-cli-lb-shapes.jsonl", + "operation": "osc/Client.DeleteLoadBalancer", + "kind": "status", + "path": "", + "reason": "A delete is instantaneous here and asynchronous upstream, so the two disagree on every read of a teardown. MEASURED AGAIN on 2026-08-24 and at much greater length than #354 could (#427): this recording's teardown retries until a read proves the object gone, so it carries 26 DeleteLoadBalancer attempts and 24 DeleteNet ones, each answered by the cloud with a 400 or a 409 while the object settled and by this emulator with a 200 at the first ask. The 'absent' findings on the same operations are the same cause seen from the body: an Errors array where the cloud refused, or an empty list where the cloud still held the object. Nothing here is a field the pack fails to serve.", + "issue": "https://github.com/stephrobert/feint/issues/380" + }, + { + "file": "outscale/oapi-cli-lb-shapes.jsonl", + "operation": "osc/Client.DeleteNet", + "kind": "absent", + "path": "Errors", + "reason": "A delete is instantaneous here and asynchronous upstream, so the two disagree on every read of a teardown. MEASURED AGAIN on 2026-08-24 and at much greater length than #354 could (#427): this recording's teardown retries until a read proves the object gone, so it carries 26 DeleteLoadBalancer attempts and 24 DeleteNet ones, each answered by the cloud with a 400 or a 409 while the object settled and by this emulator with a 200 at the first ask. The 'absent' findings on the same operations are the same cause seen from the body: an Errors array where the cloud refused, or an empty list where the cloud still held the object. Nothing here is a field the pack fails to serve.", + "issue": "https://github.com/stephrobert/feint/issues/380" + }, + { + "file": "outscale/oapi-cli-lb-shapes.jsonl", + "operation": "osc/Client.DeleteNet", + "kind": "status", + "path": "", + "reason": "A delete is instantaneous here and asynchronous upstream, so the two disagree on every read of a teardown. MEASURED AGAIN on 2026-08-24 and at much greater length than #354 could (#427): this recording's teardown retries until a read proves the object gone, so it carries 26 DeleteLoadBalancer attempts and 24 DeleteNet ones, each answered by the cloud with a 400 or a 409 while the object settled and by this emulator with a 200 at the first ask. The 'absent' findings on the same operations are the same cause seen from the body: an Errors array where the cloud refused, or an empty list where the cloud still held the object. Nothing here is a field the pack fails to serve.", + "issue": "https://github.com/stephrobert/feint/issues/380" + }, + { + "file": "outscale/oapi-cli-lb-shapes.jsonl", + "operation": "osc/Client.DeleteSecurityGroup", + "kind": "absent", + "path": "Errors", + "reason": "A delete is instantaneous here and asynchronous upstream, so the two disagree on every read of a teardown. MEASURED AGAIN on 2026-08-24 and at much greater length than #354 could (#427): this recording's teardown retries until a read proves the object gone, so it carries 26 DeleteLoadBalancer attempts and 24 DeleteNet ones, each answered by the cloud with a 400 or a 409 while the object settled and by this emulator with a 200 at the first ask. The 'absent' findings on the same operations are the same cause seen from the body: an Errors array where the cloud refused, or an empty list where the cloud still held the object. Nothing here is a field the pack fails to serve.", + "issue": "https://github.com/stephrobert/feint/issues/380" + }, + { + "file": "outscale/oapi-cli-lb-shapes.jsonl", "operation": "osc/Client.DeleteSecurityGroup", "kind": "status", "path": "", - "reason": "The cloud refuses DeleteSecurityGroup with 409 ResourceConflict for about ninety seconds after the machine that wore the group is terminated, and accepts on the thirty-first attempt; this emulator releases the group the moment the machine is terminated and answers 200 at once. A REPLAY CANNOT GRADE THE DIFFERENCE EITHER WAY: the recording is thirty refusals then an acceptance, and a corpus has no ninety seconds in it, since the sanitiser normalises every timestamp to one second apart. Reproducing the delay would make every stack teardown and every conformance run wait it out, which is a product decision rather than a defect to patch. #380 carries it, and internal/providers/outscale/securitygroups.go records the measurement where the skip is.", + "reason": "A delete is instantaneous here and asynchronous upstream, so the two disagree on every read of a teardown. MEASURED AGAIN on 2026-08-24 and at much greater length than #354 could (#427): this recording's teardown retries until a read proves the object gone, so it carries 26 DeleteLoadBalancer attempts and 24 DeleteNet ones, each answered by the cloud with a 400 or a 409 while the object settled and by this emulator with a 200 at the first ask. The 'absent' findings on the same operations are the same cause seen from the body: an Errors array where the cloud refused, or an empty list where the cloud still held the object. Nothing here is a field the pack fails to serve.", + "issue": "https://github.com/stephrobert/feint/issues/380" + }, + { + "file": "outscale/oapi-cli-lb-shapes.jsonl", + "operation": "osc/Client.DeleteSubnet", + "kind": "absent", + "path": "Errors", + "reason": "A delete is instantaneous here and asynchronous upstream, so the two disagree on every read of a teardown. MEASURED AGAIN on 2026-08-24 and at much greater length than #354 could (#427): this recording's teardown retries until a read proves the object gone, so it carries 26 DeleteLoadBalancer attempts and 24 DeleteNet ones, each answered by the cloud with a 400 or a 409 while the object settled and by this emulator with a 200 at the first ask. The 'absent' findings on the same operations are the same cause seen from the body: an Errors array where the cloud refused, or an empty list where the cloud still held the object. Nothing here is a field the pack fails to serve.", + "issue": "https://github.com/stephrobert/feint/issues/380" + }, + { + "file": "outscale/oapi-cli-lb-shapes.jsonl", + "operation": "osc/Client.DeleteSubnet", + "kind": "status", + "path": "", + "reason": "A delete is instantaneous here and asynchronous upstream, so the two disagree on every read of a teardown. MEASURED AGAIN on 2026-08-24 and at much greater length than #354 could (#427): this recording's teardown retries until a read proves the object gone, so it carries 26 DeleteLoadBalancer attempts and 24 DeleteNet ones, each answered by the cloud with a 400 or a 409 while the object settled and by this emulator with a 200 at the first ask. The 'absent' findings on the same operations are the same cause seen from the body: an Errors array where the cloud refused, or an empty list where the cloud still held the object. Nothing here is a field the pack fails to serve.", + "issue": "https://github.com/stephrobert/feint/issues/380" + }, + { + "file": "outscale/oapi-cli-lb-shapes.jsonl", + "operation": "osc/Client.ReadLoadBalancers", + "kind": "absent", + "path": "LoadBalancers[]", + "reason": "A field a real cloudgouv-eu-west-1 account answers and this emulator omits, recorded 2026-08-24 (#427). An image a client cuts from a machine carries its own device mapping and a FileLocation; a volume resize answers the TaskId that will finish it; a health check carries its Path. #437 carries the measured list and the reason each is a distinct decision.", + "issue": "https://github.com/stephrobert/feint/issues/437" + }, + { + "file": "outscale/oapi-cli-lb-shapes.jsonl", + "operation": "osc/Client.ReadLoadBalancers", + "kind": "absent", + "path": "LoadBalancers[].HealthCheck.Path", + "reason": "A field a real cloudgouv-eu-west-1 account answers and this emulator omits, recorded 2026-08-24 (#427). An image a client cuts from a machine carries its own device mapping and a FileLocation; a volume resize answers the TaskId that will finish it; a health check carries its Path. #437 carries the measured list and the reason each is a distinct decision.", + "issue": "https://github.com/stephrobert/feint/issues/437" + }, + { + "file": "outscale/oapi-cli-lb-shapes.jsonl", + "operation": "osc/Client.ReadNets", + "kind": "absent", + "path": "Nets[]", + "reason": "A field a real cloudgouv-eu-west-1 account answers and this emulator omits, recorded 2026-08-24 (#427). An image a client cuts from a machine carries its own device mapping and a FileLocation; a volume resize answers the TaskId that will finish it; a health check carries its Path. #437 carries the measured list and the reason each is a distinct decision.", + "issue": "https://github.com/stephrobert/feint/issues/437" + }, + { + "file": "outscale/oapi-cli-lb-shapes.jsonl", + "operation": "osc/Client.ReadSecurityGroups", + "kind": "absent", + "path": "SecurityGroups[]", + "reason": "A field a real cloudgouv-eu-west-1 account answers and this emulator omits, recorded 2026-08-24 (#427). An image a client cuts from a machine carries its own device mapping and a FileLocation; a volume resize answers the TaskId that will finish it; a health check carries its Path. #437 carries the measured list and the reason each is a distinct decision.", + "issue": "https://github.com/stephrobert/feint/issues/437" + }, + { + "file": "outscale/oapi-cli-lb-shapes.jsonl", + "operation": "osc/Client.ReadSubnets", + "kind": "absent", + "path": "Subnets[]", + "reason": "A field a real cloudgouv-eu-west-1 account answers and this emulator omits, recorded 2026-08-24 (#427). An image a client cuts from a machine carries its own device mapping and a FileLocation; a volume resize answers the TaskId that will finish it; a health check carries its Path. #437 carries the measured list and the reason each is a distinct decision.", + "issue": "https://github.com/stephrobert/feint/issues/437" + }, + { + "file": "outscale/oapi-cli-lb-shapes.jsonl", + "operation": "osc/Client.ReadVms", + "kind": "absent", + "path": "Vms[].BlockDeviceMappings[]", + "reason": "A machine's BlockDeviceMappings is where a client reads the root volume it must not delete. #378 served the key; this recording shows the ARRAY still answers empty where the cloud answers an element, because the emulated machine owns no root volume. Recorded 2026-08-24 (#427).", + "issue": "https://github.com/stephrobert/feint/issues/378" + }, + { + "file": "outscale/oapi-cli-lb-shapes.jsonl", + "operation": "osc/Client.UpdateLoadBalancer", + "kind": "absent", + "path": "LoadBalancer", + "reason": "A delete is instantaneous here and asynchronous upstream, so the two disagree on every read of a teardown. MEASURED AGAIN on 2026-08-24 and at much greater length than #354 could (#427): this recording's teardown retries until a read proves the object gone, so it carries 26 DeleteLoadBalancer attempts and 24 DeleteNet ones, each answered by the cloud with a 400 or a 409 while the object settled and by this emulator with a 200 at the first ask. The 'absent' findings on the same operations are the same cause seen from the body: an Errors array where the cloud refused, or an empty list where the cloud still held the object. Nothing here is a field the pack fails to serve.", + "issue": "https://github.com/stephrobert/feint/issues/380" + }, + { + "file": "outscale/oapi-cli-lb-shapes.jsonl", + "operation": "osc/Client.UpdateLoadBalancer", + "kind": "status", + "path": "", + "reason": "A delete is instantaneous here and asynchronous upstream, so the two disagree on every read of a teardown. MEASURED AGAIN on 2026-08-24 and at much greater length than #354 could (#427): this recording's teardown retries until a read proves the object gone, so it carries 26 DeleteLoadBalancer attempts and 24 DeleteNet ones, each answered by the cloud with a 400 or a 409 while the object settled and by this emulator with a 200 at the first ask. The 'absent' findings on the same operations are the same cause seen from the body: an Errors array where the cloud refused, or an empty list where the cloud still held the object. Nothing here is a field the pack fails to serve.", "issue": "https://github.com/stephrobert/feint/issues/380" }, { @@ -295,18 +431,26 @@ }, { "file": "outscale/oapi-cli-lifecycle.jsonl", - "operation": "osc/Client.ReadKeypairs", + "operation": "osc/Client.DeleteSecurityGroup", "kind": "status", "path": "", - "reason": "NOT A DIVERGENCE OF THE EMULATOR, and the recorded REQUEST is what is wrong. oapi-cli sent Filters.KeypairNames as the array FiltersKeypair declares (contracts/outscale.json); the proxy's redaction replaced the whole value with one string, because KeypairNames matches the \"key\" carrier \u2014 a price internal/proxy/redact.go names as paid knowingly. So the corpus holds {\"Filters\":{\"KeypairNames\":\"REDACTED-17\"}} and the replay reissues it verbatim. Nothing could see it: until 2026-08-28 the Outscale pack read an undecodable filter as an absent one and answered 200 with the whole inventory, so two silent defects cancelled out and this gate passed by accident. #566's type gate refuses the value with a 400, and the absent Keypairs finding on the same exchange is that 400's body. redactValue now keeps a list of scalars a list (TestARedactedListOfScalarsStaysAList), so a recording made after that change carries the array and needs neither entry \u2014 these two cover the two lines recorded before it, and the staleness rule deletes them the day this corpus is recorded again.", - "issue": "https://github.com/stephrobert/feint/issues/566" + "reason": "The cloud refuses DeleteSecurityGroup with 409 ResourceConflict for about ninety seconds after the machine that wore the group is terminated, and accepts on the thirty-first attempt; this emulator releases the group the moment the machine is terminated and answers 200 at once. A REPLAY CANNOT GRADE THE DIFFERENCE EITHER WAY: the recording is thirty refusals then an acceptance, and a corpus has no ninety seconds in it, since the sanitiser normalises every timestamp to one second apart. Reproducing the delay would make every stack teardown and every conformance run wait it out, which is a product decision rather than a defect to patch. #380 carries it, and internal/providers/outscale/securitygroups.go records the measurement where the skip is.", + "issue": "https://github.com/stephrobert/feint/issues/380" }, { "file": "outscale/oapi-cli-lifecycle.jsonl", "operation": "osc/Client.ReadKeypairs", "kind": "absent", "path": "Keypairs", - "reason": "NOT A DIVERGENCE OF THE EMULATOR, and the recorded REQUEST is what is wrong. oapi-cli sent Filters.KeypairNames as the array FiltersKeypair declares (contracts/outscale.json); the proxy's redaction replaced the whole value with one string, because KeypairNames matches the \"key\" carrier \u2014 a price internal/proxy/redact.go names as paid knowingly. So the corpus holds {\"Filters\":{\"KeypairNames\":\"REDACTED-17\"}} and the replay reissues it verbatim. Nothing could see it: until 2026-08-28 the Outscale pack read an undecodable filter as an absent one and answered 200 with the whole inventory, so two silent defects cancelled out and this gate passed by accident. #566's type gate refuses the value with a 400, and the absent Keypairs finding on the same exchange is that 400's body. redactValue now keeps a list of scalars a list (TestARedactedListOfScalarsStaysAList), so a recording made after that change carries the array and needs neither entry \u2014 these two cover the two lines recorded before it, and the staleness rule deletes them the day this corpus is recorded again.", + "reason": "NOT A DIVERGENCE OF THE EMULATOR, and the recorded REQUEST is what is wrong. oapi-cli sent Filters.KeypairNames as the array FiltersKeypair declares (contracts/outscale.json); the proxy's redaction replaced the whole value with one string, because KeypairNames matches the \"key\" carrier — a price internal/proxy/redact.go names as paid knowingly. So the corpus holds {\"Filters\":{\"KeypairNames\":\"REDACTED-17\"}} and the replay reissues it verbatim. Nothing could see it: until 2026-08-28 the Outscale pack read an undecodable filter as an absent one and answered 200 with the whole inventory, so two silent defects cancelled out and this gate passed by accident. #566's type gate refuses the value with a 400, and the absent Keypairs finding on the same exchange is that 400's body. redactValue now keeps a list of scalars a list (TestARedactedListOfScalarsStaysAList), so a recording made after that change carries the array and needs neither entry — these two cover the two lines recorded before it, and the staleness rule deletes them the day this corpus is recorded again.", + "issue": "https://github.com/stephrobert/feint/issues/566" + }, + { + "file": "outscale/oapi-cli-lifecycle.jsonl", + "operation": "osc/Client.ReadKeypairs", + "kind": "status", + "path": "", + "reason": "NOT A DIVERGENCE OF THE EMULATOR, and the recorded REQUEST is what is wrong. oapi-cli sent Filters.KeypairNames as the array FiltersKeypair declares (contracts/outscale.json); the proxy's redaction replaced the whole value with one string, because KeypairNames matches the \"key\" carrier — a price internal/proxy/redact.go names as paid knowingly. So the corpus holds {\"Filters\":{\"KeypairNames\":\"REDACTED-17\"}} and the replay reissues it verbatim. Nothing could see it: until 2026-08-28 the Outscale pack read an undecodable filter as an absent one and answered 200 with the whole inventory, so two silent defects cancelled out and this gate passed by accident. #566's type gate refuses the value with a 400, and the absent Keypairs finding on the same exchange is that 400's body. redactValue now keeps a list of scalars a list (TestARedactedListOfScalarsStaysAList), so a recording made after that change carries the array and needs neither entry — these two cover the two lines recorded before it, and the staleness rule deletes them the day this corpus is recorded again.", "issue": "https://github.com/stephrobert/feint/issues/566" }, { @@ -350,52 +494,68 @@ "issue": "https://github.com/stephrobert/feint/issues/380" }, { - "file": "scaleway/scw-instance.jsonl", - "operation": "block/v1alpha1/API.DeleteVolume", + "file": "outscale/oapi-cli-machine-shapes.jsonl", + "operation": "osc/Client.CreateImage", + "kind": "absent", + "path": "Image.BlockDeviceMappings[]", + "reason": "An image cut from a VmId answers an empty mapping array where the real account answers the device it snapshotted. Measured 2026-08-24 against cloudgouv-eu-west-1 (#427). It is NOT declined in DeclinedFields, and that is the decision: the same operation DOES serve the mappings when the client names a snapshot, so an operation-level decline would be true of one kind of object and fiction for the other — the shape #389 cost a release to understand. Serving it means cutting a snapshot of a machine's root volume, which this emulator holds no bytes for. Its neighbour Image.FileLocation left this file on 2026-08-24: it is unserved on every path, so it could be declined honestly.", + "issue": "https://github.com/stephrobert/feint/issues/437" + }, + { + "file": "outscale/oapi-cli-machine-shapes.jsonl", + "operation": "osc/Client.DeleteDhcpOptions", "kind": "status", "path": "", - "reason": "scw deletes the root volume through block/v1alpha1 after deleting the server, and the cloud answers 204 where this emulator answers 404 because the volume it created is an instance/v1 one", - "issue": "#365" + "reason": "A delete is instantaneous here and asynchronous upstream, so the two disagree on every read of a teardown. MEASURED AGAIN on 2026-08-24 and at much greater length than #354 could (#427): this recording's teardown retries until a read proves the object gone, so it carries 26 DeleteLoadBalancer attempts and 24 DeleteNet ones, each answered by the cloud with a 400 or a 409 while the object settled and by this emulator with a 200 at the first ask. The 'absent' findings on the same operations are the same cause seen from the body: an Errors array where the cloud refused, or an empty list where the cloud still held the object. Nothing here is a field the pack fails to serve.", + "issue": "https://github.com/stephrobert/feint/issues/380" }, { - "file": "scaleway/scw-instance.jsonl", - "operation": "block/v1alpha1/API.GetVolume", + "file": "outscale/oapi-cli-machine-shapes.jsonl", + "operation": "osc/Client.LinkPrivateIps", "kind": "status", "path": "", - "reason": "the cloud gave the DEV1-S an SBS root volume and answers 200 on the block path scw follows; this emulator attaches a local instance/v1 volume, so the same read answers 404", - "issue": "#365" + "reason": "A delete is instantaneous here and asynchronous upstream, so the two disagree on every read of a teardown. MEASURED AGAIN on 2026-08-24 and at much greater length than #354 could (#427): this recording's teardown retries until a read proves the object gone, so it carries 26 DeleteLoadBalancer attempts and 24 DeleteNet ones, each answered by the cloud with a 400 or a 409 while the object settled and by this emulator with a 200 at the first ask. The 'absent' findings on the same operations are the same cause seen from the body: an Errors array where the cloud refused, or an empty list where the cloud still held the object. Nothing here is a field the pack fails to serve.", + "issue": "https://github.com/stephrobert/feint/issues/380" }, { - "file": "scaleway/scw-instance.jsonl", - "operation": "block/v1alpha1/API.GetVolume", + "file": "outscale/oapi-cli-machine-shapes.jsonl", + "operation": "osc/Client.ReadVolumes", "kind": "absent", - "path": "*", - "reason": "every top-level field of a body this emulator does not serve at all: the block volume behind the root disk does not exist here, so its fourteen fields are absent for the one reason the status divergence above states", - "issue": "#365" + "path": "Volumes[]", + "reason": "A field a real cloudgouv-eu-west-1 account answers and this emulator omits, recorded 2026-08-24 (#427). An image a client cuts from a machine carries its own device mapping and a FileLocation; a volume resize answers the TaskId that will finish it; a health check carries its Path. #437 carries the measured list and the reason each is a distinct decision.", + "issue": "https://github.com/stephrobert/feint/issues/437" }, { - "file": "scaleway/scw-instance.jsonl", - "operation": "instance/v1/API.ListServers", + "file": "outscale/oapi-cli-machine-shapes.jsonl", + "operation": "osc/Client.UnlinkPrivateIps", + "kind": "status", + "path": "", + "reason": "A delete is instantaneous here and asynchronous upstream, so the two disagree on every read of a teardown. MEASURED AGAIN on 2026-08-24 and at much greater length than #354 could (#427): this recording's teardown retries until a read proves the object gone, so it carries 26 DeleteLoadBalancer attempts and 24 DeleteNet ones, each answered by the cloud with a 400 or a 409 while the object settled and by this emulator with a 200 at the first ask. The 'absent' findings on the same operations are the same cause seen from the body: an Errors array where the cloud refused, or an empty list where the cloud still held the object. Nothing here is a field the pack fails to serve.", + "issue": "https://github.com/stephrobert/feint/issues/380" + }, + { + "file": "outscale/oapi-cli-machine-shapes.jsonl", + "operation": "osc/Client.UpdateImage", "kind": "absent", - "path": "servers[]", - "reason": "the create honours the project the request named and the unfiltered list is scoped to the default project, so the server the cloud listed is invisible to the list that follows it here", - "issue": "#369" + "path": "Image.BlockDeviceMappings[]", + "reason": "An image cut from a VmId answers an empty mapping array where the real account answers the device it snapshotted. Measured 2026-08-24 against cloudgouv-eu-west-1 (#427). It is NOT declined in DeclinedFields, and that is the decision: the same operation DOES serve the mappings when the client names a snapshot, so an operation-level decline would be true of one kind of object and fiction for the other — the shape #389 cost a release to understand. Serving it means cutting a snapshot of a machine's root volume, which this emulator holds no bytes for. Its neighbour Image.FileLocation left this file on 2026-08-24: it is unserved on every path, so it could be declined honestly.", + "issue": "https://github.com/stephrobert/feint/issues/437" }, { - "file": "exoscale/exo-refusals.jsonl", - "operation": "exoscale/v2.create-private-network", + "file": "outscale/oapi-cli-machine-shapes.jsonl", + "operation": "osc/Client.UpdateNet", "kind": "absent", - "path": "errors", - "reason": "The status and the refusal are right and the body is short one key: the cloud carries an errors array naming the field it refused, this emulator carries the message alone. Adding the array unconditionally would put an empty one on the refusals the cloud answers without it, so a fix waits for a recording that shows both shapes.", - "issue": "https://github.com/stephrobert/feint/issues/397" + "path": "Net", + "reason": "A delete is instantaneous here and asynchronous upstream, so the two disagree on every read of a teardown. MEASURED AGAIN on 2026-08-24 and at much greater length than #354 could (#427): this recording's teardown retries until a read proves the object gone, so it carries 26 DeleteLoadBalancer attempts and 24 DeleteNet ones, each answered by the cloud with a 400 or a 409 while the object settled and by this emulator with a 200 at the first ask. The 'absent' findings on the same operations are the same cause seen from the body: an Errors array where the cloud refused, or an empty list where the cloud still held the object. Nothing here is a field the pack fails to serve.", + "issue": "https://github.com/stephrobert/feint/issues/380" }, { - "file": "outscale/oapi-cli-refusals.jsonl", - "operation": "osc/Client.CreateVms", + "file": "outscale/oapi-cli-machine-shapes.jsonl", + "operation": "osc/Client.UpdateNet", "kind": "status", "path": "", - "reason": "An image identifier that names nothing answers success here by decision: getImage serves the default catalogue entry for an unknown ID (docs/limits.md, so a script holding a real one keeps working), the marketplace local-image list does the same, and the Outscale createVms never looks the image up. The cloud refuses all three. A fix has to say what an emulator with no account does with an image identifier it never minted, and corpus/outscale/oapi-cli-lifecycle.jsonl is what would grade the answer.", - "issue": "https://github.com/stephrobert/feint/issues/392" + "reason": "A delete is instantaneous here and asynchronous upstream, so the two disagree on every read of a teardown. MEASURED AGAIN on 2026-08-24 and at much greater length than #354 could (#427): this recording's teardown retries until a read proves the object gone, so it carries 26 DeleteLoadBalancer attempts and 24 DeleteNet ones, each answered by the cloud with a 400 or a 409 while the object settled and by this emulator with a 200 at the first ask. The 'absent' findings on the same operations are the same cause seen from the body: an Errors array where the cloud refused, or an empty list where the cloud still held the object. Nothing here is a field the pack fails to serve.", + "issue": "https://github.com/stephrobert/feint/issues/380" }, { "file": "outscale/oapi-cli-refusals.jsonl", @@ -407,15 +567,15 @@ }, { "file": "outscale/oapi-cli-refusals.jsonl", - "operation": "osc/Client.DeleteImage", + "operation": "osc/Client.CreateVms", "kind": "status", "path": "", - "reason": "THIS IS THE INSTRUMENT, NOT THE PACK. The sanitiser mints Outscale identifiers as a counter in eight hexadecimal digits, and the Outscale catalogue's own images are ami-00000001..3, so the recording's second prefixed value came out naming a catalogue image of this emulator: the refusal recorded against an image that does not exist replays against one that does, and answers 409 instead of 400. Nothing here is a measurement of the pack until the two namespaces are disjoint.", - "issue": "https://github.com/stephrobert/feint/issues/395" + "reason": "An image identifier that names nothing answers success here by decision: getImage serves the default catalogue entry for an unknown ID (docs/limits.md, so a script holding a real one keeps working), the marketplace local-image list does the same, and the Outscale createVms never looks the image up. The cloud refuses all three. A fix has to say what an emulator with no account does with an image identifier it never minted, and corpus/outscale/oapi-cli-lifecycle.jsonl is what would grade the answer.", + "issue": "https://github.com/stephrobert/feint/issues/392" }, { "file": "outscale/oapi-cli-refusals.jsonl", - "operation": "osc/Client.UpdateImage", + "operation": "osc/Client.DeleteImage", "kind": "status", "path": "", "reason": "THIS IS THE INSTRUMENT, NOT THE PACK. The sanitiser mints Outscale identifiers as a counter in eight hexadecimal digits, and the Outscale catalogue's own images are ami-00000001..3, so the recording's second prefixed value came out naming a catalogue image of this emulator: the refusal recorded against an image that does not exist replays against one that does, and answers 409 instead of 400. Nothing here is a measurement of the pack until the two namespaces are disjoint.", @@ -424,1234 +584,1234 @@ { "file": "outscale/oapi-cli-refusals.jsonl", "operation": "osc/Client.ReadVms", - "kind": "status", - "path": "", + "kind": "absent", + "path": "Errors", "reason": "ReadVms is the only one of seventeen Outscale reads that refuses a filter value which is not an identifier; the other sixteen answer 200 with an empty list, measured in the same run. Reproducing a one-operation inconsistency is a decision, and validating all seventeen would be wrong sixteen times.", "issue": "https://github.com/stephrobert/feint/issues/396" }, { "file": "outscale/oapi-cli-refusals.jsonl", "operation": "osc/Client.ReadVms", - "kind": "absent", - "path": "Errors", + "kind": "status", + "path": "", "reason": "ReadVms is the only one of seventeen Outscale reads that refuses a filter value which is not an identifier; the other sixteen answer 200 with an empty list, measured in the same run. Reproducing a one-operation inconsistency is a decision, and validating all seventeen would be wrong sixteen times.", "issue": "https://github.com/stephrobert/feint/issues/396" }, { - "file": "scaleway/scw-refusals.jsonl", - "operation": "vpc/v2/API.CreateVPC", + "file": "outscale/oapi-cli-refusals.jsonl", + "operation": "osc/Client.UpdateImage", "kind": "status", "path": "", - "reason": "The create names a project identifier that does not exist and the cloud refuses it before allocating anything; this emulator accepts any project as an isolation boundary (projectOf, scopeOf) and creates the resource. Reproducing the refusal means the emulator must declare which project exists, which removes the multi-project behaviour it was built with, so it is a product decision rather than a patch. Recorded 2026-08-21 against a real fr-par account (#390).", - "issue": "https://github.com/stephrobert/feint/issues/391" + "reason": "THIS IS THE INSTRUMENT, NOT THE PACK. The sanitiser mints Outscale identifiers as a counter in eight hexadecimal digits, and the Outscale catalogue's own images are ami-00000001..3, so the recording's second prefixed value came out naming a catalogue image of this emulator: the refusal recorded against an image that does not exist replays against one that does, and answers 409 instead of 400. Nothing here is a measurement of the pack until the two namespaces are disjoint.", + "issue": "https://github.com/stephrobert/feint/issues/395" }, { - "file": "scaleway/scw-refusals.jsonl", - "operation": "vpc/v2/API.CreateVPC", - "kind": "absent", - "path": "message", - "reason": "The create names a project identifier that does not exist and the cloud refuses it before allocating anything; this emulator accepts any project as an isolation boundary (projectOf, scopeOf) and creates the resource. Reproducing the refusal means the emulator must declare which project exists, which removes the multi-project behaviour it was built with, so it is a product decision rather than a patch. Recorded 2026-08-21 against a real fr-par account (#390).", - "issue": "https://github.com/stephrobert/feint/issues/391" + "file": "scaleway/scw-billed-shapes.jsonl", + "operation": "block/v1alpha1/API.DeleteSnapshot", + "kind": "status", + "path": "", + "reason": "THIS EMULATOR'S LIFECYCLE TRANSITIONS ARE IMMEDIATE, which docs/limits.md states as a decision, and this is that decision seen from block/v1alpha1. A snapshot is `available` the instant it is cut here; upstream it is born in a transient state and a DeleteSnapshot issued while it settles is refused with 412 and a body. corpus/scaleway/scw-billed-shapes.jsonl seq 9-12 is exactly that sequence: the refused delete, a read that still finds the snapshot, the accepted delete, a read that does not. Here the FIRST delete succeeds, so the read between them finds nothing (404 against 200, which reports all twelve fields of the snapshot absent) and the second delete meets nothing (404 against 204). A refusal whose state is not reachable here cannot be served: a guard for a state nothing can enter is a control that can never fire, which is the rule docs/limits.md already draws for Outscale's InvalidVolumeState. It goes the day a snapshot has a settling state. Recorded 2026-08-24 (#427).", + "issue": "https://github.com/stephrobert/feint/issues/433" }, { - "file": "scaleway/scw-refusals.jsonl", - "operation": "vpc/v2/API.CreateVPC", + "file": "scaleway/scw-billed-shapes.jsonl", + "operation": "block/v1alpha1/API.DeleteSnapshot", + "kind": "type", + "path": "", + "reason": "THIS EMULATOR'S LIFECYCLE TRANSITIONS ARE IMMEDIATE, which docs/limits.md states as a decision, and this is that decision seen from block/v1alpha1. A snapshot is `available` the instant it is cut here; upstream it is born in a transient state and a DeleteSnapshot issued while it settles is refused with 412 and a body. corpus/scaleway/scw-billed-shapes.jsonl seq 9-12 is exactly that sequence: the refused delete, a read that still finds the snapshot, the accepted delete, a read that does not. Here the FIRST delete succeeds, so the read between them finds nothing (404 against 200, which reports all twelve fields of the snapshot absent) and the second delete meets nothing (404 against 204). A refusal whose state is not reachable here cannot be served: a guard for a state nothing can enter is a control that can never fire, which is the rule docs/limits.md already draws for Outscale's InvalidVolumeState. It goes the day a snapshot has a settling state. Recorded 2026-08-24 (#427).", + "issue": "https://github.com/stephrobert/feint/issues/433" + }, + { + "file": "scaleway/scw-billed-shapes.jsonl", + "operation": "block/v1alpha1/API.DeleteVolume", + "kind": "status", + "path": "", + "reason": "A SERVER'S ROOT VOLUME LIVES IN instance/v1 HERE AND IN block UPSTREAM, and every finding on this operation is that one default. corpus/scaleway/scw-billed-shapes.jsonl seq 11 is a CreateServer whose body names no volume, and fr-par answered volumes {\"0\": {volume_type: sbs_volume}} -- a block volume -- then read it three times through block/v1alpha1 (seq 13, 27, 33) and deleted it there (seq 34). This emulator gives such a server a b_ssd volume in instance/v1, so all four calls answer 404, and a 404 reports every field of the expected body as absent. sbs_volume IS honoured when a client asks for it, and tools/conformance/scaleway/terraform/main.tf asks for it, so the path is proven end to end by the real provider; what is not done is the default. MEASURED WHY NOT: flipping it reds ten tests at once, because the whole instance/v1 volume surface reads a server's root disk out of the instance store -- CreateSnapshot and CreateImage cannot find the volume to snapshot, attach-volume and detach-volume refuse it, and terminate stops carrying it away. docs/limits.md carries it. It goes the day the default moves, which is a batch of its own and not a line in a handler. Recorded 2026-08-24 (#427).", + "issue": "https://github.com/stephrobert/feint/issues/433" + }, + { + "file": "scaleway/scw-billed-shapes.jsonl", + "operation": "block/v1alpha1/API.GetSnapshot", "kind": "absent", - "path": "resource", - "reason": "The create names a project identifier that does not exist and the cloud refuses it before allocating anything; this emulator accepts any project as an isolation boundary (projectOf, scopeOf) and creates the resource. Reproducing the refusal means the emulator must declare which project exists, which removes the multi-project behaviour it was built with, so it is a product decision rather than a patch. Recorded 2026-08-21 against a real fr-par account (#390).", - "issue": "https://github.com/stephrobert/feint/issues/391" + "path": "class", + "reason": "THIS EMULATOR'S LIFECYCLE TRANSITIONS ARE IMMEDIATE, which docs/limits.md states as a decision, and this is that decision seen from block/v1alpha1. A snapshot is `available` the instant it is cut here; upstream it is born in a transient state and a DeleteSnapshot issued while it settles is refused with 412 and a body. corpus/scaleway/scw-billed-shapes.jsonl seq 9-12 is exactly that sequence: the refused delete, a read that still finds the snapshot, the accepted delete, a read that does not. Here the FIRST delete succeeds, so the read between them finds nothing (404 against 200, which reports all twelve fields of the snapshot absent) and the second delete meets nothing (404 against 204). A refusal whose state is not reachable here cannot be served: a guard for a state nothing can enter is a control that can never fire, which is the rule docs/limits.md already draws for Outscale's InvalidVolumeState. It goes the day a snapshot has a settling state. Recorded 2026-08-24 (#427).", + "issue": "https://github.com/stephrobert/feint/issues/433" }, { - "file": "scaleway/scw-refusals.jsonl", - "operation": "vpc/v2/API.CreateVPC", + "file": "scaleway/scw-billed-shapes.jsonl", + "operation": "block/v1alpha1/API.GetSnapshot", "kind": "absent", - "path": "resource_id", - "reason": "The create names a project identifier that does not exist and the cloud refuses it before allocating anything; this emulator accepts any project as an isolation boundary (projectOf, scopeOf) and creates the resource. Reproducing the refusal means the emulator must declare which project exists, which removes the multi-project behaviour it was built with, so it is a product decision rather than a patch. Recorded 2026-08-21 against a real fr-par account (#390).", - "issue": "https://github.com/stephrobert/feint/issues/391" + "path": "created_at", + "reason": "THIS EMULATOR'S LIFECYCLE TRANSITIONS ARE IMMEDIATE, which docs/limits.md states as a decision, and this is that decision seen from block/v1alpha1. A snapshot is `available` the instant it is cut here; upstream it is born in a transient state and a DeleteSnapshot issued while it settles is refused with 412 and a body. corpus/scaleway/scw-billed-shapes.jsonl seq 9-12 is exactly that sequence: the refused delete, a read that still finds the snapshot, the accepted delete, a read that does not. Here the FIRST delete succeeds, so the read between them finds nothing (404 against 200, which reports all twelve fields of the snapshot absent) and the second delete meets nothing (404 against 204). A refusal whose state is not reachable here cannot be served: a guard for a state nothing can enter is a control that can never fire, which is the rule docs/limits.md already draws for Outscale's InvalidVolumeState. It goes the day a snapshot has a settling state. Recorded 2026-08-24 (#427).", + "issue": "https://github.com/stephrobert/feint/issues/433" }, { - "file": "scaleway/scw-refusals.jsonl", - "operation": "vpc/v2/API.CreateVPC", + "file": "scaleway/scw-billed-shapes.jsonl", + "operation": "block/v1alpha1/API.GetSnapshot", "kind": "absent", - "path": "type", - "reason": "The create names a project identifier that does not exist and the cloud refuses it before allocating anything; this emulator accepts any project as an isolation boundary (projectOf, scopeOf) and creates the resource. Reproducing the refusal means the emulator must declare which project exists, which removes the multi-project behaviour it was built with, so it is a product decision rather than a patch. Recorded 2026-08-21 against a real fr-par account (#390).", - "issue": "https://github.com/stephrobert/feint/issues/391" + "path": "id", + "reason": "THIS EMULATOR'S LIFECYCLE TRANSITIONS ARE IMMEDIATE, which docs/limits.md states as a decision, and this is that decision seen from block/v1alpha1. A snapshot is `available` the instant it is cut here; upstream it is born in a transient state and a DeleteSnapshot issued while it settles is refused with 412 and a body. corpus/scaleway/scw-billed-shapes.jsonl seq 9-12 is exactly that sequence: the refused delete, a read that still finds the snapshot, the accepted delete, a read that does not. Here the FIRST delete succeeds, so the read between them finds nothing (404 against 200, which reports all twelve fields of the snapshot absent) and the second delete meets nothing (404 against 204). A refusal whose state is not reachable here cannot be served: a guard for a state nothing can enter is a control that can never fire, which is the rule docs/limits.md already draws for Outscale's InvalidVolumeState. It goes the day a snapshot has a settling state. Recorded 2026-08-24 (#427).", + "issue": "https://github.com/stephrobert/feint/issues/433" }, { - "file": "scaleway/scw-refusals.jsonl", - "operation": "instance/v1/API.GetImage", - "kind": "status", - "path": "", - "reason": "An image identifier that names nothing answers success here by decision: getImage serves the default catalogue entry for an unknown ID (docs/limits.md, so a script holding a real one keeps working), the marketplace local-image list does the same, and the Outscale createVms never looks the image up. The cloud refuses all three. A fix has to say what an emulator with no account does with an image identifier it never minted, and corpus/outscale/oapi-cli-lifecycle.jsonl is what would grade the answer.", - "issue": "https://github.com/stephrobert/feint/issues/392" + "file": "scaleway/scw-billed-shapes.jsonl", + "operation": "block/v1alpha1/API.GetSnapshot", + "kind": "absent", + "path": "name", + "reason": "THIS EMULATOR'S LIFECYCLE TRANSITIONS ARE IMMEDIATE, which docs/limits.md states as a decision, and this is that decision seen from block/v1alpha1. A snapshot is `available` the instant it is cut here; upstream it is born in a transient state and a DeleteSnapshot issued while it settles is refused with 412 and a body. corpus/scaleway/scw-billed-shapes.jsonl seq 9-12 is exactly that sequence: the refused delete, a read that still finds the snapshot, the accepted delete, a read that does not. Here the FIRST delete succeeds, so the read between them finds nothing (404 against 200, which reports all twelve fields of the snapshot absent) and the second delete meets nothing (404 against 204). A refusal whose state is not reachable here cannot be served: a guard for a state nothing can enter is a control that can never fire, which is the rule docs/limits.md already draws for Outscale's InvalidVolumeState. It goes the day a snapshot has a settling state. Recorded 2026-08-24 (#427).", + "issue": "https://github.com/stephrobert/feint/issues/433" }, { - "file": "scaleway/scw-refusals.jsonl", - "operation": "instance/v1/API.GetImage", + "file": "scaleway/scw-billed-shapes.jsonl", + "operation": "block/v1alpha1/API.GetSnapshot", "kind": "absent", - "path": "message", - "reason": "An image identifier that names nothing answers success here by decision: getImage serves the default catalogue entry for an unknown ID (docs/limits.md, so a script holding a real one keeps working), the marketplace local-image list does the same, and the Outscale createVms never looks the image up. The cloud refuses all three. A fix has to say what an emulator with no account does with an image identifier it never minted, and corpus/outscale/oapi-cli-lifecycle.jsonl is what would grade the answer.", - "issue": "https://github.com/stephrobert/feint/issues/392" + "path": "parent_volume", + "reason": "THIS EMULATOR'S LIFECYCLE TRANSITIONS ARE IMMEDIATE, which docs/limits.md states as a decision, and this is that decision seen from block/v1alpha1. A snapshot is `available` the instant it is cut here; upstream it is born in a transient state and a DeleteSnapshot issued while it settles is refused with 412 and a body. corpus/scaleway/scw-billed-shapes.jsonl seq 9-12 is exactly that sequence: the refused delete, a read that still finds the snapshot, the accepted delete, a read that does not. Here the FIRST delete succeeds, so the read between them finds nothing (404 against 200, which reports all twelve fields of the snapshot absent) and the second delete meets nothing (404 against 204). A refusal whose state is not reachable here cannot be served: a guard for a state nothing can enter is a control that can never fire, which is the rule docs/limits.md already draws for Outscale's InvalidVolumeState. It goes the day a snapshot has a settling state. Recorded 2026-08-24 (#427).", + "issue": "https://github.com/stephrobert/feint/issues/433" }, { - "file": "scaleway/scw-refusals.jsonl", - "operation": "instance/v1/API.GetImage", + "file": "scaleway/scw-billed-shapes.jsonl", + "operation": "block/v1alpha1/API.GetSnapshot", "kind": "absent", - "path": "resource", - "reason": "An image identifier that names nothing answers success here by decision: getImage serves the default catalogue entry for an unknown ID (docs/limits.md, so a script holding a real one keeps working), the marketplace local-image list does the same, and the Outscale createVms never looks the image up. The cloud refuses all three. A fix has to say what an emulator with no account does with an image identifier it never minted, and corpus/outscale/oapi-cli-lifecycle.jsonl is what would grade the answer.", - "issue": "https://github.com/stephrobert/feint/issues/392" + "path": "project_id", + "reason": "THIS EMULATOR'S LIFECYCLE TRANSITIONS ARE IMMEDIATE, which docs/limits.md states as a decision, and this is that decision seen from block/v1alpha1. A snapshot is `available` the instant it is cut here; upstream it is born in a transient state and a DeleteSnapshot issued while it settles is refused with 412 and a body. corpus/scaleway/scw-billed-shapes.jsonl seq 9-12 is exactly that sequence: the refused delete, a read that still finds the snapshot, the accepted delete, a read that does not. Here the FIRST delete succeeds, so the read between them finds nothing (404 against 200, which reports all twelve fields of the snapshot absent) and the second delete meets nothing (404 against 204). A refusal whose state is not reachable here cannot be served: a guard for a state nothing can enter is a control that can never fire, which is the rule docs/limits.md already draws for Outscale's InvalidVolumeState. It goes the day a snapshot has a settling state. Recorded 2026-08-24 (#427).", + "issue": "https://github.com/stephrobert/feint/issues/433" }, { - "file": "scaleway/scw-refusals.jsonl", - "operation": "instance/v1/API.GetImage", + "file": "scaleway/scw-billed-shapes.jsonl", + "operation": "block/v1alpha1/API.GetSnapshot", "kind": "absent", - "path": "resource_id", - "reason": "An image identifier that names nothing answers success here by decision: getImage serves the default catalogue entry for an unknown ID (docs/limits.md, so a script holding a real one keeps working), the marketplace local-image list does the same, and the Outscale createVms never looks the image up. The cloud refuses all three. A fix has to say what an emulator with no account does with an image identifier it never minted, and corpus/outscale/oapi-cli-lifecycle.jsonl is what would grade the answer.", - "issue": "https://github.com/stephrobert/feint/issues/392" + "path": "references", + "reason": "THIS EMULATOR'S LIFECYCLE TRANSITIONS ARE IMMEDIATE, which docs/limits.md states as a decision, and this is that decision seen from block/v1alpha1. A snapshot is `available` the instant it is cut here; upstream it is born in a transient state and a DeleteSnapshot issued while it settles is refused with 412 and a body. corpus/scaleway/scw-billed-shapes.jsonl seq 9-12 is exactly that sequence: the refused delete, a read that still finds the snapshot, the accepted delete, a read that does not. Here the FIRST delete succeeds, so the read between them finds nothing (404 against 200, which reports all twelve fields of the snapshot absent) and the second delete meets nothing (404 against 204). A refusal whose state is not reachable here cannot be served: a guard for a state nothing can enter is a control that can never fire, which is the rule docs/limits.md already draws for Outscale's InvalidVolumeState. It goes the day a snapshot has a settling state. Recorded 2026-08-24 (#427).", + "issue": "https://github.com/stephrobert/feint/issues/433" }, { - "file": "scaleway/scw-refusals.jsonl", - "operation": "instance/v1/API.GetImage", + "file": "scaleway/scw-billed-shapes.jsonl", + "operation": "block/v1alpha1/API.GetSnapshot", "kind": "absent", - "path": "type", - "reason": "An image identifier that names nothing answers success here by decision: getImage serves the default catalogue entry for an unknown ID (docs/limits.md, so a script holding a real one keeps working), the marketplace local-image list does the same, and the Outscale createVms never looks the image up. The cloud refuses all three. A fix has to say what an emulator with no account does with an image identifier it never minted, and corpus/outscale/oapi-cli-lifecycle.jsonl is what would grade the answer.", - "issue": "https://github.com/stephrobert/feint/issues/392" + "path": "size", + "reason": "THIS EMULATOR'S LIFECYCLE TRANSITIONS ARE IMMEDIATE, which docs/limits.md states as a decision, and this is that decision seen from block/v1alpha1. A snapshot is `available` the instant it is cut here; upstream it is born in a transient state and a DeleteSnapshot issued while it settles is refused with 412 and a body. corpus/scaleway/scw-billed-shapes.jsonl seq 9-12 is exactly that sequence: the refused delete, a read that still finds the snapshot, the accepted delete, a read that does not. Here the FIRST delete succeeds, so the read between them finds nothing (404 against 200, which reports all twelve fields of the snapshot absent) and the second delete meets nothing (404 against 204). A refusal whose state is not reachable here cannot be served: a guard for a state nothing can enter is a control that can never fire, which is the rule docs/limits.md already draws for Outscale's InvalidVolumeState. It goes the day a snapshot has a settling state. Recorded 2026-08-24 (#427).", + "issue": "https://github.com/stephrobert/feint/issues/433" }, { - "file": "scaleway/scw-refusals.jsonl", - "operation": "instance/v1/API.CreateServer", - "kind": "status", - "path": "", - "reason": "The create names a project identifier that does not exist and the cloud refuses it before allocating anything; this emulator accepts any project as an isolation boundary (projectOf, scopeOf) and creates the resource. Reproducing the refusal means the emulator must declare which project exists, which removes the multi-project behaviour it was built with, so it is a product decision rather than a patch. Recorded 2026-08-21 against a real fr-par account (#390).", - "issue": "https://github.com/stephrobert/feint/issues/391" + "file": "scaleway/scw-billed-shapes.jsonl", + "operation": "block/v1alpha1/API.GetSnapshot", + "kind": "absent", + "path": "status", + "reason": "THIS EMULATOR'S LIFECYCLE TRANSITIONS ARE IMMEDIATE, which docs/limits.md states as a decision, and this is that decision seen from block/v1alpha1. A snapshot is `available` the instant it is cut here; upstream it is born in a transient state and a DeleteSnapshot issued while it settles is refused with 412 and a body. corpus/scaleway/scw-billed-shapes.jsonl seq 9-12 is exactly that sequence: the refused delete, a read that still finds the snapshot, the accepted delete, a read that does not. Here the FIRST delete succeeds, so the read between them finds nothing (404 against 200, which reports all twelve fields of the snapshot absent) and the second delete meets nothing (404 against 204). A refusal whose state is not reachable here cannot be served: a guard for a state nothing can enter is a control that can never fire, which is the rule docs/limits.md already draws for Outscale's InvalidVolumeState. It goes the day a snapshot has a settling state. Recorded 2026-08-24 (#427).", + "issue": "https://github.com/stephrobert/feint/issues/433" }, { - "file": "scaleway/scw-refusals.jsonl", - "operation": "instance/v1/API.CreateServer", + "file": "scaleway/scw-billed-shapes.jsonl", + "operation": "block/v1alpha1/API.GetSnapshot", "kind": "absent", - "path": "details", - "reason": "The create names a project identifier that does not exist and the cloud refuses it before allocating anything; this emulator accepts any project as an isolation boundary (projectOf, scopeOf) and creates the resource. Reproducing the refusal means the emulator must declare which project exists, which removes the multi-project behaviour it was built with, so it is a product decision rather than a patch. Recorded 2026-08-21 against a real fr-par account (#390).", - "issue": "https://github.com/stephrobert/feint/issues/391" + "path": "tags", + "reason": "THIS EMULATOR'S LIFECYCLE TRANSITIONS ARE IMMEDIATE, which docs/limits.md states as a decision, and this is that decision seen from block/v1alpha1. A snapshot is `available` the instant it is cut here; upstream it is born in a transient state and a DeleteSnapshot issued while it settles is refused with 412 and a body. corpus/scaleway/scw-billed-shapes.jsonl seq 9-12 is exactly that sequence: the refused delete, a read that still finds the snapshot, the accepted delete, a read that does not. Here the FIRST delete succeeds, so the read between them finds nothing (404 against 200, which reports all twelve fields of the snapshot absent) and the second delete meets nothing (404 against 204). A refusal whose state is not reachable here cannot be served: a guard for a state nothing can enter is a control that can never fire, which is the rule docs/limits.md already draws for Outscale's InvalidVolumeState. It goes the day a snapshot has a settling state. Recorded 2026-08-24 (#427).", + "issue": "https://github.com/stephrobert/feint/issues/433" }, { - "file": "scaleway/scw-refusals.jsonl", - "operation": "instance/v1/API.CreateServer", + "file": "scaleway/scw-billed-shapes.jsonl", + "operation": "block/v1alpha1/API.GetSnapshot", "kind": "absent", - "path": "message", - "reason": "The create names a project identifier that does not exist and the cloud refuses it before allocating anything; this emulator accepts any project as an isolation boundary (projectOf, scopeOf) and creates the resource. Reproducing the refusal means the emulator must declare which project exists, which removes the multi-project behaviour it was built with, so it is a product decision rather than a patch. Recorded 2026-08-21 against a real fr-par account (#390).", - "issue": "https://github.com/stephrobert/feint/issues/391" + "path": "updated_at", + "reason": "THIS EMULATOR'S LIFECYCLE TRANSITIONS ARE IMMEDIATE, which docs/limits.md states as a decision, and this is that decision seen from block/v1alpha1. A snapshot is `available` the instant it is cut here; upstream it is born in a transient state and a DeleteSnapshot issued while it settles is refused with 412 and a body. corpus/scaleway/scw-billed-shapes.jsonl seq 9-12 is exactly that sequence: the refused delete, a read that still finds the snapshot, the accepted delete, a read that does not. Here the FIRST delete succeeds, so the read between them finds nothing (404 against 200, which reports all twelve fields of the snapshot absent) and the second delete meets nothing (404 against 204). A refusal whose state is not reachable here cannot be served: a guard for a state nothing can enter is a control that can never fire, which is the rule docs/limits.md already draws for Outscale's InvalidVolumeState. It goes the day a snapshot has a settling state. Recorded 2026-08-24 (#427).", + "issue": "https://github.com/stephrobert/feint/issues/433" }, { - "file": "scaleway/scw-refusals.jsonl", - "operation": "instance/v1/API.CreateServer", + "file": "scaleway/scw-billed-shapes.jsonl", + "operation": "block/v1alpha1/API.GetSnapshot", "kind": "absent", - "path": "type", - "reason": "The create names a project identifier that does not exist and the cloud refuses it before allocating anything; this emulator accepts any project as an isolation boundary (projectOf, scopeOf) and creates the resource. Reproducing the refusal means the emulator must declare which project exists, which removes the multi-project behaviour it was built with, so it is a product decision rather than a patch. Recorded 2026-08-21 against a real fr-par account (#390).", - "issue": "https://github.com/stephrobert/feint/issues/391" + "path": "zone", + "reason": "THIS EMULATOR'S LIFECYCLE TRANSITIONS ARE IMMEDIATE, which docs/limits.md states as a decision, and this is that decision seen from block/v1alpha1. A snapshot is `available` the instant it is cut here; upstream it is born in a transient state and a DeleteSnapshot issued while it settles is refused with 412 and a body. corpus/scaleway/scw-billed-shapes.jsonl seq 9-12 is exactly that sequence: the refused delete, a read that still finds the snapshot, the accepted delete, a read that does not. Here the FIRST delete succeeds, so the read between them finds nothing (404 against 200, which reports all twelve fields of the snapshot absent) and the second delete meets nothing (404 against 204). A refusal whose state is not reachable here cannot be served: a guard for a state nothing can enter is a control that can never fire, which is the rule docs/limits.md already draws for Outscale's InvalidVolumeState. It goes the day a snapshot has a settling state. Recorded 2026-08-24 (#427).", + "issue": "https://github.com/stephrobert/feint/issues/433" }, { - "file": "scaleway/scw-refusals.jsonl", - "operation": "instance/v1/API.CreateIP", + "file": "scaleway/scw-billed-shapes.jsonl", + "operation": "block/v1alpha1/API.GetSnapshot", "kind": "status", "path": "", - "reason": "The create names a project identifier that does not exist and the cloud refuses it before allocating anything; this emulator accepts any project as an isolation boundary (projectOf, scopeOf) and creates the resource. Reproducing the refusal means the emulator must declare which project exists, which removes the multi-project behaviour it was built with, so it is a product decision rather than a patch. Recorded 2026-08-21 against a real fr-par account (#390).", - "issue": "https://github.com/stephrobert/feint/issues/391" + "reason": "THIS EMULATOR'S LIFECYCLE TRANSITIONS ARE IMMEDIATE, which docs/limits.md states as a decision, and this is that decision seen from block/v1alpha1. A snapshot is `available` the instant it is cut here; upstream it is born in a transient state and a DeleteSnapshot issued while it settles is refused with 412 and a body. corpus/scaleway/scw-billed-shapes.jsonl seq 9-12 is exactly that sequence: the refused delete, a read that still finds the snapshot, the accepted delete, a read that does not. Here the FIRST delete succeeds, so the read between them finds nothing (404 against 200, which reports all twelve fields of the snapshot absent) and the second delete meets nothing (404 against 204). A refusal whose state is not reachable here cannot be served: a guard for a state nothing can enter is a control that can never fire, which is the rule docs/limits.md already draws for Outscale's InvalidVolumeState. It goes the day a snapshot has a settling state. Recorded 2026-08-24 (#427).", + "issue": "https://github.com/stephrobert/feint/issues/433" }, { - "file": "scaleway/scw-refusals.jsonl", - "operation": "instance/v1/API.CreateIP", + "file": "scaleway/scw-billed-shapes.jsonl", + "operation": "block/v1alpha1/API.GetVolume", "kind": "absent", - "path": "details", - "reason": "The create names a project identifier that does not exist and the cloud refuses it before allocating anything; this emulator accepts any project as an isolation boundary (projectOf, scopeOf) and creates the resource. Reproducing the refusal means the emulator must declare which project exists, which removes the multi-project behaviour it was built with, so it is a product decision rather than a patch. Recorded 2026-08-21 against a real fr-par account (#390).", - "issue": "https://github.com/stephrobert/feint/issues/391" + "path": "created_at", + "reason": "A SERVER'S ROOT VOLUME LIVES IN instance/v1 HERE AND IN block UPSTREAM, and every finding on this operation is that one default. corpus/scaleway/scw-billed-shapes.jsonl seq 11 is a CreateServer whose body names no volume, and fr-par answered volumes {\"0\": {volume_type: sbs_volume}} -- a block volume -- then read it three times through block/v1alpha1 (seq 13, 27, 33) and deleted it there (seq 34). This emulator gives such a server a b_ssd volume in instance/v1, so all four calls answer 404, and a 404 reports every field of the expected body as absent. sbs_volume IS honoured when a client asks for it, and tools/conformance/scaleway/terraform/main.tf asks for it, so the path is proven end to end by the real provider; what is not done is the default. MEASURED WHY NOT: flipping it reds ten tests at once, because the whole instance/v1 volume surface reads a server's root disk out of the instance store -- CreateSnapshot and CreateImage cannot find the volume to snapshot, attach-volume and detach-volume refuse it, and terminate stops carrying it away. docs/limits.md carries it. It goes the day the default moves, which is a batch of its own and not a line in a handler. Recorded 2026-08-24 (#427).", + "issue": "https://github.com/stephrobert/feint/issues/433" }, { - "file": "scaleway/scw-refusals.jsonl", - "operation": "instance/v1/API.CreateIP", + "file": "scaleway/scw-billed-shapes.jsonl", + "operation": "block/v1alpha1/API.GetVolume", "kind": "absent", - "path": "message", - "reason": "The create names a project identifier that does not exist and the cloud refuses it before allocating anything; this emulator accepts any project as an isolation boundary (projectOf, scopeOf) and creates the resource. Reproducing the refusal means the emulator must declare which project exists, which removes the multi-project behaviour it was built with, so it is a product decision rather than a patch. Recorded 2026-08-21 against a real fr-par account (#390).", - "issue": "https://github.com/stephrobert/feint/issues/391" + "path": "id", + "reason": "A SERVER'S ROOT VOLUME LIVES IN instance/v1 HERE AND IN block UPSTREAM, and every finding on this operation is that one default. corpus/scaleway/scw-billed-shapes.jsonl seq 11 is a CreateServer whose body names no volume, and fr-par answered volumes {\"0\": {volume_type: sbs_volume}} -- a block volume -- then read it three times through block/v1alpha1 (seq 13, 27, 33) and deleted it there (seq 34). This emulator gives such a server a b_ssd volume in instance/v1, so all four calls answer 404, and a 404 reports every field of the expected body as absent. sbs_volume IS honoured when a client asks for it, and tools/conformance/scaleway/terraform/main.tf asks for it, so the path is proven end to end by the real provider; what is not done is the default. MEASURED WHY NOT: flipping it reds ten tests at once, because the whole instance/v1 volume surface reads a server's root disk out of the instance store -- CreateSnapshot and CreateImage cannot find the volume to snapshot, attach-volume and detach-volume refuse it, and terminate stops carrying it away. docs/limits.md carries it. It goes the day the default moves, which is a batch of its own and not a line in a handler. Recorded 2026-08-24 (#427).", + "issue": "https://github.com/stephrobert/feint/issues/433" }, { - "file": "scaleway/scw-refusals.jsonl", - "operation": "instance/v1/API.CreateIP", + "file": "scaleway/scw-billed-shapes.jsonl", + "operation": "block/v1alpha1/API.GetVolume", "kind": "absent", - "path": "type", - "reason": "The create names a project identifier that does not exist and the cloud refuses it before allocating anything; this emulator accepts any project as an isolation boundary (projectOf, scopeOf) and creates the resource. Reproducing the refusal means the emulator must declare which project exists, which removes the multi-project behaviour it was built with, so it is a product decision rather than a patch. Recorded 2026-08-21 against a real fr-par account (#390).", - "issue": "https://github.com/stephrobert/feint/issues/391" + "path": "last_detached_at", + "reason": "A SERVER'S ROOT VOLUME LIVES IN instance/v1 HERE AND IN block UPSTREAM, and every finding on this operation is that one default. corpus/scaleway/scw-billed-shapes.jsonl seq 11 is a CreateServer whose body names no volume, and fr-par answered volumes {\"0\": {volume_type: sbs_volume}} -- a block volume -- then read it three times through block/v1alpha1 (seq 13, 27, 33) and deleted it there (seq 34). This emulator gives such a server a b_ssd volume in instance/v1, so all four calls answer 404, and a 404 reports every field of the expected body as absent. sbs_volume IS honoured when a client asks for it, and tools/conformance/scaleway/terraform/main.tf asks for it, so the path is proven end to end by the real provider; what is not done is the default. MEASURED WHY NOT: flipping it reds ten tests at once, because the whole instance/v1 volume surface reads a server's root disk out of the instance store -- CreateSnapshot and CreateImage cannot find the volume to snapshot, attach-volume and detach-volume refuse it, and terminate stops carrying it away. docs/limits.md carries it. It goes the day the default moves, which is a batch of its own and not a line in a handler. Recorded 2026-08-24 (#427).", + "issue": "https://github.com/stephrobert/feint/issues/433" }, { - "file": "scaleway/scw-refusals.jsonl", - "operation": "instance/v1/API.CreateVolume", - "kind": "status", - "path": "", - "reason": "The cloud has retired b_ssd and refuses it at creation with a constraint on volume_type; this emulator accepts it and, worse, mints it for every root volume (rootVolume, servers.go). It is drift the surface scan cannot see, because CreateVolume's Go signature is unchanged.", - "issue": "https://github.com/stephrobert/feint/issues/393" + "file": "scaleway/scw-billed-shapes.jsonl", + "operation": "block/v1alpha1/API.GetVolume", + "kind": "absent", + "path": "name", + "reason": "A SERVER'S ROOT VOLUME LIVES IN instance/v1 HERE AND IN block UPSTREAM, and every finding on this operation is that one default. corpus/scaleway/scw-billed-shapes.jsonl seq 11 is a CreateServer whose body names no volume, and fr-par answered volumes {\"0\": {volume_type: sbs_volume}} -- a block volume -- then read it three times through block/v1alpha1 (seq 13, 27, 33) and deleted it there (seq 34). This emulator gives such a server a b_ssd volume in instance/v1, so all four calls answer 404, and a 404 reports every field of the expected body as absent. sbs_volume IS honoured when a client asks for it, and tools/conformance/scaleway/terraform/main.tf asks for it, so the path is proven end to end by the real provider; what is not done is the default. MEASURED WHY NOT: flipping it reds ten tests at once, because the whole instance/v1 volume surface reads a server's root disk out of the instance store -- CreateSnapshot and CreateImage cannot find the volume to snapshot, attach-volume and detach-volume refuse it, and terminate stops carrying it away. docs/limits.md carries it. It goes the day the default moves, which is a batch of its own and not a line in a handler. Recorded 2026-08-24 (#427).", + "issue": "https://github.com/stephrobert/feint/issues/433" }, { - "file": "scaleway/scw-refusals.jsonl", - "operation": "instance/v1/API.CreateVolume", + "file": "scaleway/scw-billed-shapes.jsonl", + "operation": "block/v1alpha1/API.GetVolume", "kind": "absent", - "path": "details", - "reason": "The cloud has retired b_ssd and refuses it at creation with a constraint on volume_type; this emulator accepts it and, worse, mints it for every root volume (rootVolume, servers.go). It is drift the surface scan cannot see, because CreateVolume's Go signature is unchanged.", - "issue": "https://github.com/stephrobert/feint/issues/393" + "path": "parent_snapshot_id", + "reason": "A SERVER'S ROOT VOLUME LIVES IN instance/v1 HERE AND IN block UPSTREAM, and every finding on this operation is that one default. corpus/scaleway/scw-billed-shapes.jsonl seq 11 is a CreateServer whose body names no volume, and fr-par answered volumes {\"0\": {volume_type: sbs_volume}} -- a block volume -- then read it three times through block/v1alpha1 (seq 13, 27, 33) and deleted it there (seq 34). This emulator gives such a server a b_ssd volume in instance/v1, so all four calls answer 404, and a 404 reports every field of the expected body as absent. sbs_volume IS honoured when a client asks for it, and tools/conformance/scaleway/terraform/main.tf asks for it, so the path is proven end to end by the real provider; what is not done is the default. MEASURED WHY NOT: flipping it reds ten tests at once, because the whole instance/v1 volume surface reads a server's root disk out of the instance store -- CreateSnapshot and CreateImage cannot find the volume to snapshot, attach-volume and detach-volume refuse it, and terminate stops carrying it away. docs/limits.md carries it. It goes the day the default moves, which is a batch of its own and not a line in a handler. Recorded 2026-08-24 (#427).", + "issue": "https://github.com/stephrobert/feint/issues/433" }, { - "file": "scaleway/scw-refusals.jsonl", - "operation": "instance/v1/API.CreateVolume", + "file": "scaleway/scw-billed-shapes.jsonl", + "operation": "block/v1alpha1/API.GetVolume", "kind": "absent", - "path": "message", - "reason": "The cloud has retired b_ssd and refuses it at creation with a constraint on volume_type; this emulator accepts it and, worse, mints it for every root volume (rootVolume, servers.go). It is drift the surface scan cannot see, because CreateVolume's Go signature is unchanged.", - "issue": "https://github.com/stephrobert/feint/issues/393" + "path": "project_id", + "reason": "A SERVER'S ROOT VOLUME LIVES IN instance/v1 HERE AND IN block UPSTREAM, and every finding on this operation is that one default. corpus/scaleway/scw-billed-shapes.jsonl seq 11 is a CreateServer whose body names no volume, and fr-par answered volumes {\"0\": {volume_type: sbs_volume}} -- a block volume -- then read it three times through block/v1alpha1 (seq 13, 27, 33) and deleted it there (seq 34). This emulator gives such a server a b_ssd volume in instance/v1, so all four calls answer 404, and a 404 reports every field of the expected body as absent. sbs_volume IS honoured when a client asks for it, and tools/conformance/scaleway/terraform/main.tf asks for it, so the path is proven end to end by the real provider; what is not done is the default. MEASURED WHY NOT: flipping it reds ten tests at once, because the whole instance/v1 volume surface reads a server's root disk out of the instance store -- CreateSnapshot and CreateImage cannot find the volume to snapshot, attach-volume and detach-volume refuse it, and terminate stops carrying it away. docs/limits.md carries it. It goes the day the default moves, which is a batch of its own and not a line in a handler. Recorded 2026-08-24 (#427).", + "issue": "https://github.com/stephrobert/feint/issues/433" }, { - "file": "scaleway/scw-refusals.jsonl", - "operation": "instance/v1/API.CreateVolume", + "file": "scaleway/scw-billed-shapes.jsonl", + "operation": "block/v1alpha1/API.GetVolume", "kind": "absent", - "path": "type", - "reason": "The cloud has retired b_ssd and refuses it at creation with a constraint on volume_type; this emulator accepts it and, worse, mints it for every root volume (rootVolume, servers.go). It is drift the surface scan cannot see, because CreateVolume's Go signature is unchanged.", - "issue": "https://github.com/stephrobert/feint/issues/393" + "path": "references", + "reason": "A SERVER'S ROOT VOLUME LIVES IN instance/v1 HERE AND IN block UPSTREAM, and every finding on this operation is that one default. corpus/scaleway/scw-billed-shapes.jsonl seq 11 is a CreateServer whose body names no volume, and fr-par answered volumes {\"0\": {volume_type: sbs_volume}} -- a block volume -- then read it three times through block/v1alpha1 (seq 13, 27, 33) and deleted it there (seq 34). This emulator gives such a server a b_ssd volume in instance/v1, so all four calls answer 404, and a 404 reports every field of the expected body as absent. sbs_volume IS honoured when a client asks for it, and tools/conformance/scaleway/terraform/main.tf asks for it, so the path is proven end to end by the real provider; what is not done is the default. MEASURED WHY NOT: flipping it reds ten tests at once, because the whole instance/v1 volume surface reads a server's root disk out of the instance store -- CreateSnapshot and CreateImage cannot find the volume to snapshot, attach-volume and detach-volume refuse it, and terminate stops carrying it away. docs/limits.md carries it. It goes the day the default moves, which is a batch of its own and not a line in a handler. Recorded 2026-08-24 (#427).", + "issue": "https://github.com/stephrobert/feint/issues/433" }, { - "file": "scaleway/scw-refusals.jsonl", - "operation": "instance/v1/API.CreateSecurityGroup", - "kind": "status", - "path": "", - "reason": "The create names a project identifier that does not exist and the cloud refuses it before allocating anything; this emulator accepts any project as an isolation boundary (projectOf, scopeOf) and creates the resource. Reproducing the refusal means the emulator must declare which project exists, which removes the multi-project behaviour it was built with, so it is a product decision rather than a patch. Recorded 2026-08-21 against a real fr-par account (#390).", - "issue": "https://github.com/stephrobert/feint/issues/391" + "file": "scaleway/scw-billed-shapes.jsonl", + "operation": "block/v1alpha1/API.GetVolume", + "kind": "absent", + "path": "size", + "reason": "A SERVER'S ROOT VOLUME LIVES IN instance/v1 HERE AND IN block UPSTREAM, and every finding on this operation is that one default. corpus/scaleway/scw-billed-shapes.jsonl seq 11 is a CreateServer whose body names no volume, and fr-par answered volumes {\"0\": {volume_type: sbs_volume}} -- a block volume -- then read it three times through block/v1alpha1 (seq 13, 27, 33) and deleted it there (seq 34). This emulator gives such a server a b_ssd volume in instance/v1, so all four calls answer 404, and a 404 reports every field of the expected body as absent. sbs_volume IS honoured when a client asks for it, and tools/conformance/scaleway/terraform/main.tf asks for it, so the path is proven end to end by the real provider; what is not done is the default. MEASURED WHY NOT: flipping it reds ten tests at once, because the whole instance/v1 volume surface reads a server's root disk out of the instance store -- CreateSnapshot and CreateImage cannot find the volume to snapshot, attach-volume and detach-volume refuse it, and terminate stops carrying it away. docs/limits.md carries it. It goes the day the default moves, which is a batch of its own and not a line in a handler. Recorded 2026-08-24 (#427).", + "issue": "https://github.com/stephrobert/feint/issues/433" }, { - "file": "scaleway/scw-refusals.jsonl", - "operation": "instance/v1/API.CreateSecurityGroup", + "file": "scaleway/scw-billed-shapes.jsonl", + "operation": "block/v1alpha1/API.GetVolume", "kind": "absent", - "path": "details", - "reason": "The create names a project identifier that does not exist and the cloud refuses it before allocating anything; this emulator accepts any project as an isolation boundary (projectOf, scopeOf) and creates the resource. Reproducing the refusal means the emulator must declare which project exists, which removes the multi-project behaviour it was built with, so it is a product decision rather than a patch. Recorded 2026-08-21 against a real fr-par account (#390).", - "issue": "https://github.com/stephrobert/feint/issues/391" + "path": "specs", + "reason": "A SERVER'S ROOT VOLUME LIVES IN instance/v1 HERE AND IN block UPSTREAM, and every finding on this operation is that one default. corpus/scaleway/scw-billed-shapes.jsonl seq 11 is a CreateServer whose body names no volume, and fr-par answered volumes {\"0\": {volume_type: sbs_volume}} -- a block volume -- then read it three times through block/v1alpha1 (seq 13, 27, 33) and deleted it there (seq 34). This emulator gives such a server a b_ssd volume in instance/v1, so all four calls answer 404, and a 404 reports every field of the expected body as absent. sbs_volume IS honoured when a client asks for it, and tools/conformance/scaleway/terraform/main.tf asks for it, so the path is proven end to end by the real provider; what is not done is the default. MEASURED WHY NOT: flipping it reds ten tests at once, because the whole instance/v1 volume surface reads a server's root disk out of the instance store -- CreateSnapshot and CreateImage cannot find the volume to snapshot, attach-volume and detach-volume refuse it, and terminate stops carrying it away. docs/limits.md carries it. It goes the day the default moves, which is a batch of its own and not a line in a handler. Recorded 2026-08-24 (#427).", + "issue": "https://github.com/stephrobert/feint/issues/433" }, { - "file": "scaleway/scw-refusals.jsonl", - "operation": "instance/v1/API.CreateSecurityGroup", + "file": "scaleway/scw-billed-shapes.jsonl", + "operation": "block/v1alpha1/API.GetVolume", "kind": "absent", - "path": "message", - "reason": "The create names a project identifier that does not exist and the cloud refuses it before allocating anything; this emulator accepts any project as an isolation boundary (projectOf, scopeOf) and creates the resource. Reproducing the refusal means the emulator must declare which project exists, which removes the multi-project behaviour it was built with, so it is a product decision rather than a patch. Recorded 2026-08-21 against a real fr-par account (#390).", - "issue": "https://github.com/stephrobert/feint/issues/391" + "path": "status", + "reason": "A SERVER'S ROOT VOLUME LIVES IN instance/v1 HERE AND IN block UPSTREAM, and every finding on this operation is that one default. corpus/scaleway/scw-billed-shapes.jsonl seq 11 is a CreateServer whose body names no volume, and fr-par answered volumes {\"0\": {volume_type: sbs_volume}} -- a block volume -- then read it three times through block/v1alpha1 (seq 13, 27, 33) and deleted it there (seq 34). This emulator gives such a server a b_ssd volume in instance/v1, so all four calls answer 404, and a 404 reports every field of the expected body as absent. sbs_volume IS honoured when a client asks for it, and tools/conformance/scaleway/terraform/main.tf asks for it, so the path is proven end to end by the real provider; what is not done is the default. MEASURED WHY NOT: flipping it reds ten tests at once, because the whole instance/v1 volume surface reads a server's root disk out of the instance store -- CreateSnapshot and CreateImage cannot find the volume to snapshot, attach-volume and detach-volume refuse it, and terminate stops carrying it away. docs/limits.md carries it. It goes the day the default moves, which is a batch of its own and not a line in a handler. Recorded 2026-08-24 (#427).", + "issue": "https://github.com/stephrobert/feint/issues/433" }, { - "file": "scaleway/scw-refusals.jsonl", - "operation": "instance/v1/API.CreateSecurityGroup", + "file": "scaleway/scw-billed-shapes.jsonl", + "operation": "block/v1alpha1/API.GetVolume", "kind": "absent", - "path": "type", - "reason": "The create names a project identifier that does not exist and the cloud refuses it before allocating anything; this emulator accepts any project as an isolation boundary (projectOf, scopeOf) and creates the resource. Reproducing the refusal means the emulator must declare which project exists, which removes the multi-project behaviour it was built with, so it is a product decision rather than a patch. Recorded 2026-08-21 against a real fr-par account (#390).", - "issue": "https://github.com/stephrobert/feint/issues/391" + "path": "tags", + "reason": "A SERVER'S ROOT VOLUME LIVES IN instance/v1 HERE AND IN block UPSTREAM, and every finding on this operation is that one default. corpus/scaleway/scw-billed-shapes.jsonl seq 11 is a CreateServer whose body names no volume, and fr-par answered volumes {\"0\": {volume_type: sbs_volume}} -- a block volume -- then read it three times through block/v1alpha1 (seq 13, 27, 33) and deleted it there (seq 34). This emulator gives such a server a b_ssd volume in instance/v1, so all four calls answer 404, and a 404 reports every field of the expected body as absent. sbs_volume IS honoured when a client asks for it, and tools/conformance/scaleway/terraform/main.tf asks for it, so the path is proven end to end by the real provider; what is not done is the default. MEASURED WHY NOT: flipping it reds ten tests at once, because the whole instance/v1 volume surface reads a server's root disk out of the instance store -- CreateSnapshot and CreateImage cannot find the volume to snapshot, attach-volume and detach-volume refuse it, and terminate stops carrying it away. docs/limits.md carries it. It goes the day the default moves, which is a batch of its own and not a line in a handler. Recorded 2026-08-24 (#427).", + "issue": "https://github.com/stephrobert/feint/issues/433" }, { - "file": "scaleway/scw-refusals.jsonl", - "operation": "instance/v1/API.CreatePlacementGroup", + "file": "scaleway/scw-billed-shapes.jsonl", + "operation": "block/v1alpha1/API.GetVolume", + "kind": "absent", + "path": "updated_at", + "reason": "A SERVER'S ROOT VOLUME LIVES IN instance/v1 HERE AND IN block UPSTREAM, and every finding on this operation is that one default. corpus/scaleway/scw-billed-shapes.jsonl seq 11 is a CreateServer whose body names no volume, and fr-par answered volumes {\"0\": {volume_type: sbs_volume}} -- a block volume -- then read it three times through block/v1alpha1 (seq 13, 27, 33) and deleted it there (seq 34). This emulator gives such a server a b_ssd volume in instance/v1, so all four calls answer 404, and a 404 reports every field of the expected body as absent. sbs_volume IS honoured when a client asks for it, and tools/conformance/scaleway/terraform/main.tf asks for it, so the path is proven end to end by the real provider; what is not done is the default. MEASURED WHY NOT: flipping it reds ten tests at once, because the whole instance/v1 volume surface reads a server's root disk out of the instance store -- CreateSnapshot and CreateImage cannot find the volume to snapshot, attach-volume and detach-volume refuse it, and terminate stops carrying it away. docs/limits.md carries it. It goes the day the default moves, which is a batch of its own and not a line in a handler. Recorded 2026-08-24 (#427).", + "issue": "https://github.com/stephrobert/feint/issues/433" + }, + { + "file": "scaleway/scw-billed-shapes.jsonl", + "operation": "block/v1alpha1/API.GetVolume", + "kind": "absent", + "path": "zone", + "reason": "A SERVER'S ROOT VOLUME LIVES IN instance/v1 HERE AND IN block UPSTREAM, and every finding on this operation is that one default. corpus/scaleway/scw-billed-shapes.jsonl seq 11 is a CreateServer whose body names no volume, and fr-par answered volumes {\"0\": {volume_type: sbs_volume}} -- a block volume -- then read it three times through block/v1alpha1 (seq 13, 27, 33) and deleted it there (seq 34). This emulator gives such a server a b_ssd volume in instance/v1, so all four calls answer 404, and a 404 reports every field of the expected body as absent. sbs_volume IS honoured when a client asks for it, and tools/conformance/scaleway/terraform/main.tf asks for it, so the path is proven end to end by the real provider; what is not done is the default. MEASURED WHY NOT: flipping it reds ten tests at once, because the whole instance/v1 volume surface reads a server's root disk out of the instance store -- CreateSnapshot and CreateImage cannot find the volume to snapshot, attach-volume and detach-volume refuse it, and terminate stops carrying it away. docs/limits.md carries it. It goes the day the default moves, which is a batch of its own and not a line in a handler. Recorded 2026-08-24 (#427).", + "issue": "https://github.com/stephrobert/feint/issues/433" + }, + { + "file": "scaleway/scw-billed-shapes.jsonl", + "operation": "block/v1alpha1/API.GetVolume", "kind": "status", "path": "", - "reason": "The create names a project identifier that does not exist and the cloud refuses it before allocating anything; this emulator accepts any project as an isolation boundary (projectOf, scopeOf) and creates the resource. Reproducing the refusal means the emulator must declare which project exists, which removes the multi-project behaviour it was built with, so it is a product decision rather than a patch. Recorded 2026-08-21 against a real fr-par account (#390).", - "issue": "https://github.com/stephrobert/feint/issues/391" + "reason": "A SERVER'S ROOT VOLUME LIVES IN instance/v1 HERE AND IN block UPSTREAM, and every finding on this operation is that one default. corpus/scaleway/scw-billed-shapes.jsonl seq 11 is a CreateServer whose body names no volume, and fr-par answered volumes {\"0\": {volume_type: sbs_volume}} -- a block volume -- then read it three times through block/v1alpha1 (seq 13, 27, 33) and deleted it there (seq 34). This emulator gives such a server a b_ssd volume in instance/v1, so all four calls answer 404, and a 404 reports every field of the expected body as absent. sbs_volume IS honoured when a client asks for it, and tools/conformance/scaleway/terraform/main.tf asks for it, so the path is proven end to end by the real provider; what is not done is the default. MEASURED WHY NOT: flipping it reds ten tests at once, because the whole instance/v1 volume surface reads a server's root disk out of the instance store -- CreateSnapshot and CreateImage cannot find the volume to snapshot, attach-volume and detach-volume refuse it, and terminate stops carrying it away. docs/limits.md carries it. It goes the day the default moves, which is a batch of its own and not a line in a handler. Recorded 2026-08-24 (#427).", + "issue": "https://github.com/stephrobert/feint/issues/433" }, { - "file": "scaleway/scw-refusals.jsonl", - "operation": "instance/v1/API.CreatePlacementGroup", + "file": "scaleway/scw-billed-shapes.jsonl", + "operation": "lb/v1/ZonedAPI.GetLB", "kind": "absent", - "path": "details", - "reason": "The create names a project identifier that does not exist and the cloud refuses it before allocating anything; this emulator accepts any project as an isolation boundary (projectOf, scopeOf) and creates the resource. Reproducing the refusal means the emulator must declare which project exists, which removes the multi-project behaviour it was built with, so it is a product decision rather than a patch. Recorded 2026-08-21 against a real fr-par account (#390).", - "issue": "https://github.com/stephrobert/feint/issues/391" + "path": "backend_count", + "reason": "THIS EMULATOR'S LIFECYCLE TRANSITIONS ARE IMMEDIATE, and this is that decision seen from lb/v1. A DeleteLB removes the balancer at once here; upstream the read that FOLLOWS the delete still answers 200 with status `to_delete`, and only the read after that answers 404. corpus/scaleway/scw-billed-shapes.jsonl seq 47-50 is the delete, one 200, then two 404s. So one read of the recording meets a 404 here where the cloud answered a whole balancer, and a 404 reports every field of that body as absent -- nineteen findings for one instant. Reproducing it means giving a delete a terminal state that a later read clears, which is a change to the lifecycle of every kind and to what `nothing left behind` means for every conformance suite -- the same argument #380 records for Outscale, one cloud out. It goes the day a delete is asynchronous here. Recorded 2026-08-24 (#427).", + "issue": "https://github.com/stephrobert/feint/issues/434" }, { - "file": "scaleway/scw-refusals.jsonl", - "operation": "instance/v1/API.CreatePlacementGroup", + "file": "scaleway/scw-billed-shapes.jsonl", + "operation": "lb/v1/ZonedAPI.GetLB", "kind": "absent", - "path": "message", - "reason": "The create names a project identifier that does not exist and the cloud refuses it before allocating anything; this emulator accepts any project as an isolation boundary (projectOf, scopeOf) and creates the resource. Reproducing the refusal means the emulator must declare which project exists, which removes the multi-project behaviour it was built with, so it is a product decision rather than a patch. Recorded 2026-08-21 against a real fr-par account (#390).", - "issue": "https://github.com/stephrobert/feint/issues/391" + "path": "created_at", + "reason": "THIS EMULATOR'S LIFECYCLE TRANSITIONS ARE IMMEDIATE, and this is that decision seen from lb/v1. A DeleteLB removes the balancer at once here; upstream the read that FOLLOWS the delete still answers 200 with status `to_delete`, and only the read after that answers 404. corpus/scaleway/scw-billed-shapes.jsonl seq 47-50 is the delete, one 200, then two 404s. So one read of the recording meets a 404 here where the cloud answered a whole balancer, and a 404 reports every field of that body as absent -- nineteen findings for one instant. Reproducing it means giving a delete a terminal state that a later read clears, which is a change to the lifecycle of every kind and to what `nothing left behind` means for every conformance suite -- the same argument #380 records for Outscale, one cloud out. It goes the day a delete is asynchronous here. Recorded 2026-08-24 (#427).", + "issue": "https://github.com/stephrobert/feint/issues/434" }, { - "file": "scaleway/scw-refusals.jsonl", - "operation": "instance/v1/API.CreatePlacementGroup", + "file": "scaleway/scw-billed-shapes.jsonl", + "operation": "lb/v1/ZonedAPI.GetLB", "kind": "absent", - "path": "type", - "reason": "The create names a project identifier that does not exist and the cloud refuses it before allocating anything; this emulator accepts any project as an isolation boundary (projectOf, scopeOf) and creates the resource. Reproducing the refusal means the emulator must declare which project exists, which removes the multi-project behaviour it was built with, so it is a product decision rather than a patch. Recorded 2026-08-21 against a real fr-par account (#390).", - "issue": "https://github.com/stephrobert/feint/issues/391" + "path": "description", + "reason": "THIS EMULATOR'S LIFECYCLE TRANSITIONS ARE IMMEDIATE, and this is that decision seen from lb/v1. A DeleteLB removes the balancer at once here; upstream the read that FOLLOWS the delete still answers 200 with status `to_delete`, and only the read after that answers 404. corpus/scaleway/scw-billed-shapes.jsonl seq 47-50 is the delete, one 200, then two 404s. So one read of the recording meets a 404 here where the cloud answered a whole balancer, and a 404 reports every field of that body as absent -- nineteen findings for one instant. Reproducing it means giving a delete a terminal state that a later read clears, which is a change to the lifecycle of every kind and to what `nothing left behind` means for every conformance suite -- the same argument #380 records for Outscale, one cloud out. It goes the day a delete is asynchronous here. Recorded 2026-08-24 (#427).", + "issue": "https://github.com/stephrobert/feint/issues/434" }, { - "file": "scaleway/scw-refusals.jsonl", - "operation": "vpc/v2/API.CreateRoute", - "kind": "status", - "path": "", - "reason": "Both sides refuse and disagree on which refusal it is: upstream validates the body before it resolves the parent (400 naming the field) where this pack resolves the parent first (404), and lb answers 403 where the frontend is simply absent. A client branches on the status, so the difference is not cosmetic; changing it is a rule about every handler rather than about these two.", - "issue": "https://github.com/stephrobert/feint/issues/394" + "file": "scaleway/scw-billed-shapes.jsonl", + "operation": "lb/v1/ZonedAPI.GetLB", + "kind": "absent", + "path": "frontend_count", + "reason": "THIS EMULATOR'S LIFECYCLE TRANSITIONS ARE IMMEDIATE, and this is that decision seen from lb/v1. A DeleteLB removes the balancer at once here; upstream the read that FOLLOWS the delete still answers 200 with status `to_delete`, and only the read after that answers 404. corpus/scaleway/scw-billed-shapes.jsonl seq 47-50 is the delete, one 200, then two 404s. So one read of the recording meets a 404 here where the cloud answered a whole balancer, and a 404 reports every field of that body as absent -- nineteen findings for one instant. Reproducing it means giving a delete a terminal state that a later read clears, which is a change to the lifecycle of every kind and to what `nothing left behind` means for every conformance suite -- the same argument #380 records for Outscale, one cloud out. It goes the day a delete is asynchronous here. Recorded 2026-08-24 (#427).", + "issue": "https://github.com/stephrobert/feint/issues/434" }, { - "file": "scaleway/scw-refusals.jsonl", - "operation": "vpc/v2/API.CreateRoute", + "file": "scaleway/scw-billed-shapes.jsonl", + "operation": "lb/v1/ZonedAPI.GetLB", "kind": "absent", - "path": "details", - "reason": "Both sides refuse and disagree on which refusal it is: upstream validates the body before it resolves the parent (400 naming the field) where this pack resolves the parent first (404), and lb answers 403 where the frontend is simply absent. A client branches on the status, so the difference is not cosmetic; changing it is a rule about every handler rather than about these two.", - "issue": "https://github.com/stephrobert/feint/issues/394" + "path": "id", + "reason": "THIS EMULATOR'S LIFECYCLE TRANSITIONS ARE IMMEDIATE, and this is that decision seen from lb/v1. A DeleteLB removes the balancer at once here; upstream the read that FOLLOWS the delete still answers 200 with status `to_delete`, and only the read after that answers 404. corpus/scaleway/scw-billed-shapes.jsonl seq 47-50 is the delete, one 200, then two 404s. So one read of the recording meets a 404 here where the cloud answered a whole balancer, and a 404 reports every field of that body as absent -- nineteen findings for one instant. Reproducing it means giving a delete a terminal state that a later read clears, which is a change to the lifecycle of every kind and to what `nothing left behind` means for every conformance suite -- the same argument #380 records for Outscale, one cloud out. It goes the day a delete is asynchronous here. Recorded 2026-08-24 (#427).", + "issue": "https://github.com/stephrobert/feint/issues/434" }, { - "file": "scaleway/scw-refusals.jsonl", - "operation": "lb/v1/ZonedAPI.CreateLB", - "kind": "status", - "path": "", - "reason": "The create names a project identifier that does not exist and the cloud refuses it before allocating anything; this emulator accepts any project as an isolation boundary (projectOf, scopeOf) and creates the resource. Reproducing the refusal means the emulator must declare which project exists, which removes the multi-project behaviour it was built with, so it is a product decision rather than a patch. Recorded 2026-08-21 against a real fr-par account (#390).", - "issue": "https://github.com/stephrobert/feint/issues/391" + "file": "scaleway/scw-billed-shapes.jsonl", + "operation": "lb/v1/ZonedAPI.GetLB", + "kind": "absent", + "path": "instances", + "reason": "THIS EMULATOR'S LIFECYCLE TRANSITIONS ARE IMMEDIATE, and this is that decision seen from lb/v1. A DeleteLB removes the balancer at once here; upstream the read that FOLLOWS the delete still answers 200 with status `to_delete`, and only the read after that answers 404. corpus/scaleway/scw-billed-shapes.jsonl seq 47-50 is the delete, one 200, then two 404s. So one read of the recording meets a 404 here where the cloud answered a whole balancer, and a 404 reports every field of that body as absent -- nineteen findings for one instant. Reproducing it means giving a delete a terminal state that a later read clears, which is a change to the lifecycle of every kind and to what `nothing left behind` means for every conformance suite -- the same argument #380 records for Outscale, one cloud out. It goes the day a delete is asynchronous here. Recorded 2026-08-24 (#427).", + "issue": "https://github.com/stephrobert/feint/issues/434" }, { - "file": "scaleway/scw-refusals.jsonl", - "operation": "lb/v1/ZonedAPI.CreateLB", + "file": "scaleway/scw-billed-shapes.jsonl", + "operation": "lb/v1/ZonedAPI.GetLB", "kind": "absent", - "path": "details", - "reason": "The create names a project identifier that does not exist and the cloud refuses it before allocating anything; this emulator accepts any project as an isolation boundary (projectOf, scopeOf) and creates the resource. Reproducing the refusal means the emulator must declare which project exists, which removes the multi-project behaviour it was built with, so it is a product decision rather than a patch. Recorded 2026-08-21 against a real fr-par account (#390).", - "issue": "https://github.com/stephrobert/feint/issues/391" + "path": "ip", + "reason": "THIS EMULATOR'S LIFECYCLE TRANSITIONS ARE IMMEDIATE, and this is that decision seen from lb/v1. A DeleteLB removes the balancer at once here; upstream the read that FOLLOWS the delete still answers 200 with status `to_delete`, and only the read after that answers 404. corpus/scaleway/scw-billed-shapes.jsonl seq 47-50 is the delete, one 200, then two 404s. So one read of the recording meets a 404 here where the cloud answered a whole balancer, and a 404 reports every field of that body as absent -- nineteen findings for one instant. Reproducing it means giving a delete a terminal state that a later read clears, which is a change to the lifecycle of every kind and to what `nothing left behind` means for every conformance suite -- the same argument #380 records for Outscale, one cloud out. It goes the day a delete is asynchronous here. Recorded 2026-08-24 (#427).", + "issue": "https://github.com/stephrobert/feint/issues/434" }, { - "file": "scaleway/scw-refusals.jsonl", - "operation": "lb/v1/ZonedAPI.CreateLB", + "file": "scaleway/scw-billed-shapes.jsonl", + "operation": "lb/v1/ZonedAPI.GetLB", "kind": "absent", - "path": "message", - "reason": "The create names a project identifier that does not exist and the cloud refuses it before allocating anything; this emulator accepts any project as an isolation boundary (projectOf, scopeOf) and creates the resource. Reproducing the refusal means the emulator must declare which project exists, which removes the multi-project behaviour it was built with, so it is a product decision rather than a patch. Recorded 2026-08-21 against a real fr-par account (#390).", - "issue": "https://github.com/stephrobert/feint/issues/391" + "path": "name", + "reason": "THIS EMULATOR'S LIFECYCLE TRANSITIONS ARE IMMEDIATE, and this is that decision seen from lb/v1. A DeleteLB removes the balancer at once here; upstream the read that FOLLOWS the delete still answers 200 with status `to_delete`, and only the read after that answers 404. corpus/scaleway/scw-billed-shapes.jsonl seq 47-50 is the delete, one 200, then two 404s. So one read of the recording meets a 404 here where the cloud answered a whole balancer, and a 404 reports every field of that body as absent -- nineteen findings for one instant. Reproducing it means giving a delete a terminal state that a later read clears, which is a change to the lifecycle of every kind and to what `nothing left behind` means for every conformance suite -- the same argument #380 records for Outscale, one cloud out. It goes the day a delete is asynchronous here. Recorded 2026-08-24 (#427).", + "issue": "https://github.com/stephrobert/feint/issues/434" }, { - "file": "scaleway/scw-refusals.jsonl", - "operation": "lb/v1/ZonedAPI.CreateIP", - "kind": "status", - "path": "", - "reason": "The create names a project identifier that does not exist and the cloud refuses it before allocating anything; this emulator accepts any project as an isolation boundary (projectOf, scopeOf) and creates the resource. Reproducing the refusal means the emulator must declare which project exists, which removes the multi-project behaviour it was built with, so it is a product decision rather than a patch. Recorded 2026-08-21 against a real fr-par account (#390).", - "issue": "https://github.com/stephrobert/feint/issues/391" + "file": "scaleway/scw-billed-shapes.jsonl", + "operation": "lb/v1/ZonedAPI.GetLB", + "kind": "absent", + "path": "organization_id", + "reason": "THIS EMULATOR'S LIFECYCLE TRANSITIONS ARE IMMEDIATE, and this is that decision seen from lb/v1. A DeleteLB removes the balancer at once here; upstream the read that FOLLOWS the delete still answers 200 with status `to_delete`, and only the read after that answers 404. corpus/scaleway/scw-billed-shapes.jsonl seq 47-50 is the delete, one 200, then two 404s. So one read of the recording meets a 404 here where the cloud answered a whole balancer, and a 404 reports every field of that body as absent -- nineteen findings for one instant. Reproducing it means giving a delete a terminal state that a later read clears, which is a change to the lifecycle of every kind and to what `nothing left behind` means for every conformance suite -- the same argument #380 records for Outscale, one cloud out. It goes the day a delete is asynchronous here. Recorded 2026-08-24 (#427).", + "issue": "https://github.com/stephrobert/feint/issues/434" }, { - "file": "scaleway/scw-refusals.jsonl", - "operation": "lb/v1/ZonedAPI.CreateIP", + "file": "scaleway/scw-billed-shapes.jsonl", + "operation": "lb/v1/ZonedAPI.GetLB", + "kind": "absent", + "path": "private_network_count", + "reason": "THIS EMULATOR'S LIFECYCLE TRANSITIONS ARE IMMEDIATE, and this is that decision seen from lb/v1. A DeleteLB removes the balancer at once here; upstream the read that FOLLOWS the delete still answers 200 with status `to_delete`, and only the read after that answers 404. corpus/scaleway/scw-billed-shapes.jsonl seq 47-50 is the delete, one 200, then two 404s. So one read of the recording meets a 404 here where the cloud answered a whole balancer, and a 404 reports every field of that body as absent -- nineteen findings for one instant. Reproducing it means giving a delete a terminal state that a later read clears, which is a change to the lifecycle of every kind and to what `nothing left behind` means for every conformance suite -- the same argument #380 records for Outscale, one cloud out. It goes the day a delete is asynchronous here. Recorded 2026-08-24 (#427).", + "issue": "https://github.com/stephrobert/feint/issues/434" + }, + { + "file": "scaleway/scw-billed-shapes.jsonl", + "operation": "lb/v1/ZonedAPI.GetLB", + "kind": "absent", + "path": "project_id", + "reason": "THIS EMULATOR'S LIFECYCLE TRANSITIONS ARE IMMEDIATE, and this is that decision seen from lb/v1. A DeleteLB removes the balancer at once here; upstream the read that FOLLOWS the delete still answers 200 with status `to_delete`, and only the read after that answers 404. corpus/scaleway/scw-billed-shapes.jsonl seq 47-50 is the delete, one 200, then two 404s. So one read of the recording meets a 404 here where the cloud answered a whole balancer, and a 404 reports every field of that body as absent -- nineteen findings for one instant. Reproducing it means giving a delete a terminal state that a later read clears, which is a change to the lifecycle of every kind and to what `nothing left behind` means for every conformance suite -- the same argument #380 records for Outscale, one cloud out. It goes the day a delete is asynchronous here. Recorded 2026-08-24 (#427).", + "issue": "https://github.com/stephrobert/feint/issues/434" + }, + { + "file": "scaleway/scw-billed-shapes.jsonl", + "operation": "lb/v1/ZonedAPI.GetLB", + "kind": "absent", + "path": "region", + "reason": "THIS EMULATOR'S LIFECYCLE TRANSITIONS ARE IMMEDIATE, and this is that decision seen from lb/v1. A DeleteLB removes the balancer at once here; upstream the read that FOLLOWS the delete still answers 200 with status `to_delete`, and only the read after that answers 404. corpus/scaleway/scw-billed-shapes.jsonl seq 47-50 is the delete, one 200, then two 404s. So one read of the recording meets a 404 here where the cloud answered a whole balancer, and a 404 reports every field of that body as absent -- nineteen findings for one instant. Reproducing it means giving a delete a terminal state that a later read clears, which is a change to the lifecycle of every kind and to what `nothing left behind` means for every conformance suite -- the same argument #380 records for Outscale, one cloud out. It goes the day a delete is asynchronous here. Recorded 2026-08-24 (#427).", + "issue": "https://github.com/stephrobert/feint/issues/434" + }, + { + "file": "scaleway/scw-billed-shapes.jsonl", + "operation": "lb/v1/ZonedAPI.GetLB", "kind": "absent", - "path": "details", - "reason": "The create names a project identifier that does not exist and the cloud refuses it before allocating anything; this emulator accepts any project as an isolation boundary (projectOf, scopeOf) and creates the resource. Reproducing the refusal means the emulator must declare which project exists, which removes the multi-project behaviour it was built with, so it is a product decision rather than a patch. Recorded 2026-08-21 against a real fr-par account (#390).", - "issue": "https://github.com/stephrobert/feint/issues/391" + "path": "route_count", + "reason": "THIS EMULATOR'S LIFECYCLE TRANSITIONS ARE IMMEDIATE, and this is that decision seen from lb/v1. A DeleteLB removes the balancer at once here; upstream the read that FOLLOWS the delete still answers 200 with status `to_delete`, and only the read after that answers 404. corpus/scaleway/scw-billed-shapes.jsonl seq 47-50 is the delete, one 200, then two 404s. So one read of the recording meets a 404 here where the cloud answered a whole balancer, and a 404 reports every field of that body as absent -- nineteen findings for one instant. Reproducing it means giving a delete a terminal state that a later read clears, which is a change to the lifecycle of every kind and to what `nothing left behind` means for every conformance suite -- the same argument #380 records for Outscale, one cloud out. It goes the day a delete is asynchronous here. Recorded 2026-08-24 (#427).", + "issue": "https://github.com/stephrobert/feint/issues/434" }, { - "file": "scaleway/scw-refusals.jsonl", - "operation": "lb/v1/ZonedAPI.CreateIP", + "file": "scaleway/scw-billed-shapes.jsonl", + "operation": "lb/v1/ZonedAPI.GetLB", "kind": "absent", - "path": "message", - "reason": "The create names a project identifier that does not exist and the cloud refuses it before allocating anything; this emulator accepts any project as an isolation boundary (projectOf, scopeOf) and creates the resource. Reproducing the refusal means the emulator must declare which project exists, which removes the multi-project behaviour it was built with, so it is a product decision rather than a patch. Recorded 2026-08-21 against a real fr-par account (#390).", - "issue": "https://github.com/stephrobert/feint/issues/391" + "path": "ssl_compatibility_level", + "reason": "THIS EMULATOR'S LIFECYCLE TRANSITIONS ARE IMMEDIATE, and this is that decision seen from lb/v1. A DeleteLB removes the balancer at once here; upstream the read that FOLLOWS the delete still answers 200 with status `to_delete`, and only the read after that answers 404. corpus/scaleway/scw-billed-shapes.jsonl seq 47-50 is the delete, one 200, then two 404s. So one read of the recording meets a 404 here where the cloud answered a whole balancer, and a 404 reports every field of that body as absent -- nineteen findings for one instant. Reproducing it means giving a delete a terminal state that a later read clears, which is a change to the lifecycle of every kind and to what `nothing left behind` means for every conformance suite -- the same argument #380 records for Outscale, one cloud out. It goes the day a delete is asynchronous here. Recorded 2026-08-24 (#427).", + "issue": "https://github.com/stephrobert/feint/issues/434" }, { - "file": "scaleway/scw-refusals.jsonl", - "operation": "lb/v1/ZonedAPI.CreateIP", + "file": "scaleway/scw-billed-shapes.jsonl", + "operation": "lb/v1/ZonedAPI.GetLB", "kind": "absent", - "path": "type", - "reason": "The create names a project identifier that does not exist and the cloud refuses it before allocating anything; this emulator accepts any project as an isolation boundary (projectOf, scopeOf) and creates the resource. Reproducing the refusal means the emulator must declare which project exists, which removes the multi-project behaviour it was built with, so it is a product decision rather than a patch. Recorded 2026-08-21 against a real fr-par account (#390).", - "issue": "https://github.com/stephrobert/feint/issues/391" + "path": "status", + "reason": "THIS EMULATOR'S LIFECYCLE TRANSITIONS ARE IMMEDIATE, and this is that decision seen from lb/v1. A DeleteLB removes the balancer at once here; upstream the read that FOLLOWS the delete still answers 200 with status `to_delete`, and only the read after that answers 404. corpus/scaleway/scw-billed-shapes.jsonl seq 47-50 is the delete, one 200, then two 404s. So one read of the recording meets a 404 here where the cloud answered a whole balancer, and a 404 reports every field of that body as absent -- nineteen findings for one instant. Reproducing it means giving a delete a terminal state that a later read clears, which is a change to the lifecycle of every kind and to what `nothing left behind` means for every conformance suite -- the same argument #380 records for Outscale, one cloud out. It goes the day a delete is asynchronous here. Recorded 2026-08-24 (#427).", + "issue": "https://github.com/stephrobert/feint/issues/434" }, { - "file": "scaleway/scw-refusals.jsonl", - "operation": "lb/v1/ZonedAPI.CreateRoute", - "kind": "status", - "path": "", - "reason": "Both sides refuse and disagree on which refusal it is: upstream validates the body before it resolves the parent (400 naming the field) where this pack resolves the parent first (404), and lb answers 403 where the frontend is simply absent. A client branches on the status, so the difference is not cosmetic; changing it is a rule about every handler rather than about these two.", - "issue": "https://github.com/stephrobert/feint/issues/394" + "file": "scaleway/scw-billed-shapes.jsonl", + "operation": "lb/v1/ZonedAPI.GetLB", + "kind": "absent", + "path": "subscriber", + "reason": "THIS EMULATOR'S LIFECYCLE TRANSITIONS ARE IMMEDIATE, and this is that decision seen from lb/v1. A DeleteLB removes the balancer at once here; upstream the read that FOLLOWS the delete still answers 200 with status `to_delete`, and only the read after that answers 404. corpus/scaleway/scw-billed-shapes.jsonl seq 47-50 is the delete, one 200, then two 404s. So one read of the recording meets a 404 here where the cloud answered a whole balancer, and a 404 reports every field of that body as absent -- nineteen findings for one instant. Reproducing it means giving a delete a terminal state that a later read clears, which is a change to the lifecycle of every kind and to what `nothing left behind` means for every conformance suite -- the same argument #380 records for Outscale, one cloud out. It goes the day a delete is asynchronous here. Recorded 2026-08-24 (#427).", + "issue": "https://github.com/stephrobert/feint/issues/434" }, { - "file": "scaleway/scw-refusals.jsonl", - "operation": "vpcgw/v2/API.CreateGateway", - "kind": "status", - "path": "", - "reason": "The create names a project identifier that does not exist and the cloud refuses it before allocating anything; this emulator accepts any project as an isolation boundary (projectOf, scopeOf) and creates the resource. Reproducing the refusal means the emulator must declare which project exists, which removes the multi-project behaviour it was built with, so it is a product decision rather than a patch. Recorded 2026-08-21 against a real fr-par account (#390).", - "issue": "https://github.com/stephrobert/feint/issues/391" + "file": "scaleway/scw-billed-shapes.jsonl", + "operation": "lb/v1/ZonedAPI.GetLB", + "kind": "absent", + "path": "tags", + "reason": "THIS EMULATOR'S LIFECYCLE TRANSITIONS ARE IMMEDIATE, and this is that decision seen from lb/v1. A DeleteLB removes the balancer at once here; upstream the read that FOLLOWS the delete still answers 200 with status `to_delete`, and only the read after that answers 404. corpus/scaleway/scw-billed-shapes.jsonl seq 47-50 is the delete, one 200, then two 404s. So one read of the recording meets a 404 here where the cloud answered a whole balancer, and a 404 reports every field of that body as absent -- nineteen findings for one instant. Reproducing it means giving a delete a terminal state that a later read clears, which is a change to the lifecycle of every kind and to what `nothing left behind` means for every conformance suite -- the same argument #380 records for Outscale, one cloud out. It goes the day a delete is asynchronous here. Recorded 2026-08-24 (#427).", + "issue": "https://github.com/stephrobert/feint/issues/434" }, { - "file": "scaleway/scw-refusals.jsonl", - "operation": "vpcgw/v2/API.CreateGateway", + "file": "scaleway/scw-billed-shapes.jsonl", + "operation": "lb/v1/ZonedAPI.GetLB", "kind": "absent", - "path": "resource", - "reason": "The create names a project identifier that does not exist and the cloud refuses it before allocating anything; this emulator accepts any project as an isolation boundary (projectOf, scopeOf) and creates the resource. Reproducing the refusal means the emulator must declare which project exists, which removes the multi-project behaviour it was built with, so it is a product decision rather than a patch. Recorded 2026-08-21 against a real fr-par account (#390).", - "issue": "https://github.com/stephrobert/feint/issues/391" + "path": "updated_at", + "reason": "THIS EMULATOR'S LIFECYCLE TRANSITIONS ARE IMMEDIATE, and this is that decision seen from lb/v1. A DeleteLB removes the balancer at once here; upstream the read that FOLLOWS the delete still answers 200 with status `to_delete`, and only the read after that answers 404. corpus/scaleway/scw-billed-shapes.jsonl seq 47-50 is the delete, one 200, then two 404s. So one read of the recording meets a 404 here where the cloud answered a whole balancer, and a 404 reports every field of that body as absent -- nineteen findings for one instant. Reproducing it means giving a delete a terminal state that a later read clears, which is a change to the lifecycle of every kind and to what `nothing left behind` means for every conformance suite -- the same argument #380 records for Outscale, one cloud out. It goes the day a delete is asynchronous here. Recorded 2026-08-24 (#427).", + "issue": "https://github.com/stephrobert/feint/issues/434" }, { - "file": "scaleway/scw-refusals.jsonl", - "operation": "vpcgw/v2/API.CreateGateway", + "file": "scaleway/scw-billed-shapes.jsonl", + "operation": "lb/v1/ZonedAPI.GetLB", "kind": "absent", - "path": "resource_id", - "reason": "The create names a project identifier that does not exist and the cloud refuses it before allocating anything; this emulator accepts any project as an isolation boundary (projectOf, scopeOf) and creates the resource. Reproducing the refusal means the emulator must declare which project exists, which removes the multi-project behaviour it was built with, so it is a product decision rather than a patch. Recorded 2026-08-21 against a real fr-par account (#390).", - "issue": "https://github.com/stephrobert/feint/issues/391" + "path": "zone", + "reason": "THIS EMULATOR'S LIFECYCLE TRANSITIONS ARE IMMEDIATE, and this is that decision seen from lb/v1. A DeleteLB removes the balancer at once here; upstream the read that FOLLOWS the delete still answers 200 with status `to_delete`, and only the read after that answers 404. corpus/scaleway/scw-billed-shapes.jsonl seq 47-50 is the delete, one 200, then two 404s. So one read of the recording meets a 404 here where the cloud answered a whole balancer, and a 404 reports every field of that body as absent -- nineteen findings for one instant. Reproducing it means giving a delete a terminal state that a later read clears, which is a change to the lifecycle of every kind and to what `nothing left behind` means for every conformance suite -- the same argument #380 records for Outscale, one cloud out. It goes the day a delete is asynchronous here. Recorded 2026-08-24 (#427).", + "issue": "https://github.com/stephrobert/feint/issues/434" }, { - "file": "scaleway/scw-refusals.jsonl", - "operation": "vpcgw/v2/API.CreateIP", + "file": "scaleway/scw-billed-shapes.jsonl", + "operation": "lb/v1/ZonedAPI.GetLB", "kind": "status", "path": "", - "reason": "The create names a project identifier that does not exist and the cloud refuses it before allocating anything; this emulator accepts any project as an isolation boundary (projectOf, scopeOf) and creates the resource. Reproducing the refusal means the emulator must declare which project exists, which removes the multi-project behaviour it was built with, so it is a product decision rather than a patch. Recorded 2026-08-21 against a real fr-par account (#390).", - "issue": "https://github.com/stephrobert/feint/issues/391" - }, - { - "file": "scaleway/scw-refusals.jsonl", - "operation": "vpcgw/v2/API.CreateIP", - "kind": "absent", - "path": "message", - "reason": "The create names a project identifier that does not exist and the cloud refuses it before allocating anything; this emulator accepts any project as an isolation boundary (projectOf, scopeOf) and creates the resource. Reproducing the refusal means the emulator must declare which project exists, which removes the multi-project behaviour it was built with, so it is a product decision rather than a patch. Recorded 2026-08-21 against a real fr-par account (#390).", - "issue": "https://github.com/stephrobert/feint/issues/391" + "reason": "THIS EMULATOR'S LIFECYCLE TRANSITIONS ARE IMMEDIATE, and this is that decision seen from lb/v1. A DeleteLB removes the balancer at once here; upstream the read that FOLLOWS the delete still answers 200 with status `to_delete`, and only the read after that answers 404. corpus/scaleway/scw-billed-shapes.jsonl seq 47-50 is the delete, one 200, then two 404s. So one read of the recording meets a 404 here where the cloud answered a whole balancer, and a 404 reports every field of that body as absent -- nineteen findings for one instant. Reproducing it means giving a delete a terminal state that a later read clears, which is a change to the lifecycle of every kind and to what `nothing left behind` means for every conformance suite -- the same argument #380 records for Outscale, one cloud out. It goes the day a delete is asynchronous here. Recorded 2026-08-24 (#427).", + "issue": "https://github.com/stephrobert/feint/issues/434" }, { - "file": "scaleway/scw-refusals.jsonl", - "operation": "vpcgw/v2/API.CreateIP", + "file": "scaleway/scw-billed-shapes.jsonl", + "operation": "vpc/v2/API.CreatePrivateNetwork", "kind": "absent", - "path": "resource", - "reason": "The create names a project identifier that does not exist and the cloud refuses it before allocating anything; this emulator accepts any project as an isolation boundary (projectOf, scopeOf) and creates the resource. Reproducing the refusal means the emulator must declare which project exists, which removes the multi-project behaviour it was built with, so it is a product decision rather than a patch. Recorded 2026-08-21 against a real fr-par account (#390).", - "issue": "https://github.com/stephrobert/feint/issues/391" + "path": "has_s3_integration", + "reason": "THE RECORDING PREDATES THE RENAME, AND ONLY AN ACCOUNT CAN RE-RECORD IT. Scaleway renamed the vpc/v2 Object Storage family on 2026-08-25: the VPC's s3_integration_enabled became object_storage_private_access_enabled and the Private Network's has_s3_integration became has_object_storage_private_access. Both upstream sources agree and neither declares a deprecation alias, read on 2026-08-28: .upstream/scaleway-sdk-go/api/vpc/v2/vpc_sdk.go:1024 and :646, and .upstream/scaleway-openapi/vpc-v2.yml, whose x-properties-order -- the document's own exhaustive property list for each object -- carries the new name and not the old one. So the emulator answers the new name alone; answering both would invent a deprecation window no source declares. This corpus was recorded on 2026-08-20, five days before, so it is evidence of what the cloud answered BEFORE the rename and it still settles the two things a rename does not touch: the field is present on every answer, and its value is false on an account with nothing attached. Re-recording needs a paid vpc/v2 account this repository does not have, and `mise run corpus:cloud` is what would arbitrate it. It goes the day that recording is made. (#570)", + "issue": "https://github.com/stephrobert/feint/issues/570" }, { - "file": "scaleway/scw-refusals.jsonl", - "operation": "vpcgw/v2/API.CreateIP", + "file": "scaleway/scw-billed-shapes.jsonl", + "operation": "vpc/v2/API.CreateVPC", "kind": "absent", - "path": "resource_id", - "reason": "The create names a project identifier that does not exist and the cloud refuses it before allocating anything; this emulator accepts any project as an isolation boundary (projectOf, scopeOf) and creates the resource. Reproducing the refusal means the emulator must declare which project exists, which removes the multi-project behaviour it was built with, so it is a product decision rather than a patch. Recorded 2026-08-21 against a real fr-par account (#390).", - "issue": "https://github.com/stephrobert/feint/issues/391" + "path": "s3_integration_enabled", + "reason": "THE RECORDING PREDATES THE RENAME, AND ONLY AN ACCOUNT CAN RE-RECORD IT. Scaleway renamed the vpc/v2 Object Storage family on 2026-08-25: the VPC's s3_integration_enabled became object_storage_private_access_enabled and the Private Network's has_s3_integration became has_object_storage_private_access. Both upstream sources agree and neither declares a deprecation alias, read on 2026-08-28: .upstream/scaleway-sdk-go/api/vpc/v2/vpc_sdk.go:1024 and :646, and .upstream/scaleway-openapi/vpc-v2.yml, whose x-properties-order -- the document's own exhaustive property list for each object -- carries the new name and not the old one. So the emulator answers the new name alone; answering both would invent a deprecation window no source declares. This corpus was recorded on 2026-08-20, five days before, so it is evidence of what the cloud answered BEFORE the rename and it still settles the two things a rename does not touch: the field is present on every answer, and its value is false on an account with nothing attached. Re-recording needs a paid vpc/v2 account this repository does not have, and `mise run corpus:cloud` is what would arbitrate it. It goes the day that recording is made. (#570)", + "issue": "https://github.com/stephrobert/feint/issues/570" }, { - "file": "scaleway/scw-refusals.jsonl", - "operation": "vpcgw/v2/API.CreateIP", + "file": "scaleway/scw-billed-shapes.jsonl", + "operation": "vpcgw/v2/API.CreateGatewayNetwork", "kind": "absent", - "path": "type", - "reason": "The create names a project identifier that does not exist and the cloud refuses it before allocating anything; this emulator accepts any project as an isolation boundary (projectOf, scopeOf) and creates the resource. Reproducing the refusal means the emulator must declare which project exists, which removes the multi-project behaviour it was built with, so it is a product decision rather than a patch. Recorded 2026-08-21 against a real fr-par account (#390).", - "issue": "https://github.com/stephrobert/feint/issues/391" - }, - { - "file": "scaleway/scw-refusals.jsonl", - "operation": "marketplace/v2/API.ListLocalImages", - "kind": "status", - "path": "", - "reason": "An image identifier that names nothing answers success here by decision: getImage serves the default catalogue entry for an unknown ID (docs/limits.md, so a script holding a real one keeps working), the marketplace local-image list does the same, and the Outscale createVms never looks the image up. The cloud refuses all three. A fix has to say what an emulator with no account does with an image identifier it never minted, and corpus/outscale/oapi-cli-lifecycle.jsonl is what would grade the answer.", - "issue": "https://github.com/stephrobert/feint/issues/392" + "path": "current_state", + "reason": "THIS EMULATOR'S LIFECYCLE TRANSITIONS ARE IMMEDIATE, and this is that decision seen from vpcgw/v2. A gateway is `running` the instant it is created here; upstream it is `allocating` for a few seconds, and both an UpdateGateway and a CreateGatewayNetwork issued in that window are refused with 409 and a body naming the state (current_state: allocating, plus message, resource, resource_id and type). corpus/scaleway/scw-billed-shapes.jsonl seq 40 and 41 are those two refusals, and seq 39 and 51-56 show the gateway moving from allocating to running around them. Both succeed here, so the status differs and the error body's own fields read as absent -- the `absent` findings on these two operations are the same cause seen from the body, not fields the pack fails to serve. Serving the refusal would mean inventing the window, and a guard for a state nothing can enter is a control that can never fire. It goes the day a gateway allocates. Recorded 2026-08-24 (#427).", + "issue": "https://github.com/stephrobert/feint/issues/435" }, { - "file": "scaleway/scw-refusals.jsonl", - "operation": "marketplace/v2/API.ListLocalImages", + "file": "scaleway/scw-billed-shapes.jsonl", + "operation": "vpcgw/v2/API.CreateGatewayNetwork", "kind": "absent", "path": "message", - "reason": "An image identifier that names nothing answers success here by decision: getImage serves the default catalogue entry for an unknown ID (docs/limits.md, so a script holding a real one keeps working), the marketplace local-image list does the same, and the Outscale createVms never looks the image up. The cloud refuses all three. A fix has to say what an emulator with no account does with an image identifier it never minted, and corpus/outscale/oapi-cli-lifecycle.jsonl is what would grade the answer.", - "issue": "https://github.com/stephrobert/feint/issues/392" + "reason": "THIS EMULATOR'S LIFECYCLE TRANSITIONS ARE IMMEDIATE, and this is that decision seen from vpcgw/v2. A gateway is `running` the instant it is created here; upstream it is `allocating` for a few seconds, and both an UpdateGateway and a CreateGatewayNetwork issued in that window are refused with 409 and a body naming the state (current_state: allocating, plus message, resource, resource_id and type). corpus/scaleway/scw-billed-shapes.jsonl seq 40 and 41 are those two refusals, and seq 39 and 51-56 show the gateway moving from allocating to running around them. Both succeed here, so the status differs and the error body's own fields read as absent -- the `absent` findings on these two operations are the same cause seen from the body, not fields the pack fails to serve. Serving the refusal would mean inventing the window, and a guard for a state nothing can enter is a control that can never fire. It goes the day a gateway allocates. Recorded 2026-08-24 (#427).", + "issue": "https://github.com/stephrobert/feint/issues/435" }, { - "file": "scaleway/scw-refusals.jsonl", - "operation": "marketplace/v2/API.ListLocalImages", + "file": "scaleway/scw-billed-shapes.jsonl", + "operation": "vpcgw/v2/API.CreateGatewayNetwork", "kind": "absent", "path": "resource", - "reason": "An image identifier that names nothing answers success here by decision: getImage serves the default catalogue entry for an unknown ID (docs/limits.md, so a script holding a real one keeps working), the marketplace local-image list does the same, and the Outscale createVms never looks the image up. The cloud refuses all three. A fix has to say what an emulator with no account does with an image identifier it never minted, and corpus/outscale/oapi-cli-lifecycle.jsonl is what would grade the answer.", - "issue": "https://github.com/stephrobert/feint/issues/392" + "reason": "THIS EMULATOR'S LIFECYCLE TRANSITIONS ARE IMMEDIATE, and this is that decision seen from vpcgw/v2. A gateway is `running` the instant it is created here; upstream it is `allocating` for a few seconds, and both an UpdateGateway and a CreateGatewayNetwork issued in that window are refused with 409 and a body naming the state (current_state: allocating, plus message, resource, resource_id and type). corpus/scaleway/scw-billed-shapes.jsonl seq 40 and 41 are those two refusals, and seq 39 and 51-56 show the gateway moving from allocating to running around them. Both succeed here, so the status differs and the error body's own fields read as absent -- the `absent` findings on these two operations are the same cause seen from the body, not fields the pack fails to serve. Serving the refusal would mean inventing the window, and a guard for a state nothing can enter is a control that can never fire. It goes the day a gateway allocates. Recorded 2026-08-24 (#427).", + "issue": "https://github.com/stephrobert/feint/issues/435" }, { - "file": "scaleway/scw-refusals.jsonl", - "operation": "marketplace/v2/API.ListLocalImages", + "file": "scaleway/scw-billed-shapes.jsonl", + "operation": "vpcgw/v2/API.CreateGatewayNetwork", "kind": "absent", "path": "resource_id", - "reason": "An image identifier that names nothing answers success here by decision: getImage serves the default catalogue entry for an unknown ID (docs/limits.md, so a script holding a real one keeps working), the marketplace local-image list does the same, and the Outscale createVms never looks the image up. The cloud refuses all three. A fix has to say what an emulator with no account does with an image identifier it never minted, and corpus/outscale/oapi-cli-lifecycle.jsonl is what would grade the answer.", - "issue": "https://github.com/stephrobert/feint/issues/392" + "reason": "THIS EMULATOR'S LIFECYCLE TRANSITIONS ARE IMMEDIATE, and this is that decision seen from vpcgw/v2. A gateway is `running` the instant it is created here; upstream it is `allocating` for a few seconds, and both an UpdateGateway and a CreateGatewayNetwork issued in that window are refused with 409 and a body naming the state (current_state: allocating, plus message, resource, resource_id and type). corpus/scaleway/scw-billed-shapes.jsonl seq 40 and 41 are those two refusals, and seq 39 and 51-56 show the gateway moving from allocating to running around them. Both succeed here, so the status differs and the error body's own fields read as absent -- the `absent` findings on these two operations are the same cause seen from the body, not fields the pack fails to serve. Serving the refusal would mean inventing the window, and a guard for a state nothing can enter is a control that can never fire. It goes the day a gateway allocates. Recorded 2026-08-24 (#427).", + "issue": "https://github.com/stephrobert/feint/issues/435" }, { - "file": "scaleway/scw-refusals.jsonl", - "operation": "marketplace/v2/API.ListLocalImages", + "file": "scaleway/scw-billed-shapes.jsonl", + "operation": "vpcgw/v2/API.CreateGatewayNetwork", "kind": "absent", "path": "type", - "reason": "An image identifier that names nothing answers success here by decision: getImage serves the default catalogue entry for an unknown ID (docs/limits.md, so a script holding a real one keeps working), the marketplace local-image list does the same, and the Outscale createVms never looks the image up. The cloud refuses all three. A fix has to say what an emulator with no account does with an image identifier it never minted, and corpus/outscale/oapi-cli-lifecycle.jsonl is what would grade the answer.", - "issue": "https://github.com/stephrobert/feint/issues/392" + "reason": "THIS EMULATOR'S LIFECYCLE TRANSITIONS ARE IMMEDIATE, and this is that decision seen from vpcgw/v2. A gateway is `running` the instant it is created here; upstream it is `allocating` for a few seconds, and both an UpdateGateway and a CreateGatewayNetwork issued in that window are refused with 409 and a body naming the state (current_state: allocating, plus message, resource, resource_id and type). corpus/scaleway/scw-billed-shapes.jsonl seq 40 and 41 are those two refusals, and seq 39 and 51-56 show the gateway moving from allocating to running around them. Both succeed here, so the status differs and the error body's own fields read as absent -- the `absent` findings on these two operations are the same cause seen from the body, not fields the pack fails to serve. Serving the refusal would mean inventing the window, and a guard for a state nothing can enter is a control that can never fire. It goes the day a gateway allocates. Recorded 2026-08-24 (#427).", + "issue": "https://github.com/stephrobert/feint/issues/435" }, { - "operation": "instance/v1/API.ListSecurityGroupRules", - "kind": "absent", - "path": "rules[]", - "reason": "The cloud folds its six account-wide default rules into every group's list, and this emulator does not. MEASURED, and the SDK settles what they are: instance_sdk.go documents ListDefaultSecurityGroupRules as \"Lists the default rules applied to all the security groups\", and corpus/scaleway/scw-free-shapes.jsonl seq 18 and seq 20 answer the SAME six rule identifiers for a group created seconds earlier and for the `default` segment -- they are one rule set, not a copy per group. This emulator now serves it at the door that names it (#432 closed that half), and deliberately does not fold it into a group: the six are outbound TCP drops on the submission ports, nothing here filters an outbound packet, and firewallSpecOf turns every rule of a group into a runtime ACL rule -- so folding them in would push a filter to Incus that the emulator would then be enforcing for real, and would offer a client six rules to delete that it never created. It goes the day the machine layer enforces the SMTP block, which is what would make the list true. Recorded 2026-08-24 against a real fr-par account (#427).", - "issue": "https://github.com/stephrobert/feint/issues/432", - "file": "scaleway/scw-free-shapes.jsonl" + "file": "scaleway/scw-billed-shapes.jsonl", + "operation": "vpcgw/v2/API.CreateGatewayNetwork", + "kind": "status", + "path": "", + "reason": "THIS EMULATOR'S LIFECYCLE TRANSITIONS ARE IMMEDIATE, and this is that decision seen from vpcgw/v2. A gateway is `running` the instant it is created here; upstream it is `allocating` for a few seconds, and both an UpdateGateway and a CreateGatewayNetwork issued in that window are refused with 409 and a body naming the state (current_state: allocating, plus message, resource, resource_id and type). corpus/scaleway/scw-billed-shapes.jsonl seq 40 and 41 are those two refusals, and seq 39 and 51-56 show the gateway moving from allocating to running around them. Both succeed here, so the status differs and the error body's own fields read as absent -- the `absent` findings on these two operations are the same cause seen from the body, not fields the pack fails to serve. Serving the refusal would mean inventing the window, and a guard for a state nothing can enter is a control that can never fire. It goes the day a gateway allocates. Recorded 2026-08-24 (#427).", + "issue": "https://github.com/stephrobert/feint/issues/435" }, { - "operation": "ipam/v1/API.ListIPs", + "file": "scaleway/scw-billed-shapes.jsonl", + "operation": "vpcgw/v2/API.DeleteIP", "kind": "absent", - "path": "ips[]", - "reason": "The recorded BookIP names a project and the recorded ListIPs names none, so the cloud answered one address and this emulator answers an empty list. MEASURED RATHER THAN ASSUMED, and the first hypothesis was wrong: this is not an artefact of the sanitiser. Asked directly, the emulator's ListIPs?project_id= answers the address correctly, and only the UNFILTERED list is empty -- it scopes to the default project while the create honoured the project the request named. That is exactly the cause #369 already classifies for instance/v1 createServer and listServers, one product out. It is recorded here against ipam/v1 rather than folded into #369's Scaleway entries because the key must name the operation it excuses. Recorded 2026-08-24 against a real fr-par account (#427).", - "issue": "https://github.com/stephrobert/feint/issues/369", - "file": "scaleway/scw-free-shapes.jsonl" + "path": "message", + "reason": "THIS IS THE RECORDING, NOT THE PACK, and the recording proves it in its own sequence numbers. The gateway goes from 200 on its seventh read to 404 on its eighth WITH NO DELETE ANYWHERE IN THE FILE, and its address follows: the recorded DeleteIP answers 404 and the recorded GetIP answers 404 too. The destruction did not travel through the proxy. STATED RATHER THAN ASSUMED: corpus/scaleway/scw-billed-shapes.jsonl holds three recording sessions whose own sequence numbers run 1..14, 1..43 and 1..65 with no gap, so nothing was dropped between the request that read the gateway and the request that could not find it, and no DELETE on /vpc-gw/v2/.../gateways/ exists in that file at all. This emulator was never asked to delete the gateway, so it still answers it -- and its DeleteIP refuses with 400 because the address is still attached to a gateway nobody destroyed. Answering 404 for an object nobody destroyed would be the lie this project exists to avoid. The `absent` findings on these operations are the fields of the cloud's 404 body, which is the same cause seen from the body. It goes when the gateway is recorded again with its destruction in the transcript. Recorded 2026-08-24 (#427).", + "issue": "https://github.com/stephrobert/feint/issues/435" }, { "file": "scaleway/scw-billed-shapes.jsonl", - "operation": "block/v1alpha1/API.DeleteSnapshot", + "operation": "vpcgw/v2/API.DeleteIP", "kind": "status", "path": "", - "reason": "THIS EMULATOR'S LIFECYCLE TRANSITIONS ARE IMMEDIATE, which docs/limits.md states as a decision, and this is that decision seen from block/v1alpha1. A snapshot is `available` the instant it is cut here; upstream it is born in a transient state and a DeleteSnapshot issued while it settles is refused with 412 and a body. corpus/scaleway/scw-billed-shapes.jsonl seq 9-12 is exactly that sequence: the refused delete, a read that still finds the snapshot, the accepted delete, a read that does not. Here the FIRST delete succeeds, so the read between them finds nothing (404 against 200, which reports all twelve fields of the snapshot absent) and the second delete meets nothing (404 against 204). A refusal whose state is not reachable here cannot be served: a guard for a state nothing can enter is a control that can never fire, which is the rule docs/limits.md already draws for Outscale's InvalidVolumeState. It goes the day a snapshot has a settling state. Recorded 2026-08-24 (#427).", - "issue": "https://github.com/stephrobert/feint/issues/433" + "reason": "THIS IS THE RECORDING, NOT THE PACK, and the recording proves it in its own sequence numbers. The gateway goes from 200 on its seventh read to 404 on its eighth WITH NO DELETE ANYWHERE IN THE FILE, and its address follows: the recorded DeleteIP answers 404 and the recorded GetIP answers 404 too. The destruction did not travel through the proxy. STATED RATHER THAN ASSUMED: corpus/scaleway/scw-billed-shapes.jsonl holds three recording sessions whose own sequence numbers run 1..14, 1..43 and 1..65 with no gap, so nothing was dropped between the request that read the gateway and the request that could not find it, and no DELETE on /vpc-gw/v2/.../gateways/ exists in that file at all. This emulator was never asked to delete the gateway, so it still answers it -- and its DeleteIP refuses with 400 because the address is still attached to a gateway nobody destroyed. Answering 404 for an object nobody destroyed would be the lie this project exists to avoid. The `absent` findings on these operations are the fields of the cloud's 404 body, which is the same cause seen from the body. It goes when the gateway is recorded again with its destruction in the transcript. Recorded 2026-08-24 (#427).", + "issue": "https://github.com/stephrobert/feint/issues/435" }, { "file": "scaleway/scw-billed-shapes.jsonl", - "operation": "block/v1alpha1/API.GetSnapshot", - "kind": "status", - "path": "", - "reason": "THIS EMULATOR'S LIFECYCLE TRANSITIONS ARE IMMEDIATE, which docs/limits.md states as a decision, and this is that decision seen from block/v1alpha1. A snapshot is `available` the instant it is cut here; upstream it is born in a transient state and a DeleteSnapshot issued while it settles is refused with 412 and a body. corpus/scaleway/scw-billed-shapes.jsonl seq 9-12 is exactly that sequence: the refused delete, a read that still finds the snapshot, the accepted delete, a read that does not. Here the FIRST delete succeeds, so the read between them finds nothing (404 against 200, which reports all twelve fields of the snapshot absent) and the second delete meets nothing (404 against 204). A refusal whose state is not reachable here cannot be served: a guard for a state nothing can enter is a control that can never fire, which is the rule docs/limits.md already draws for Outscale's InvalidVolumeState. It goes the day a snapshot has a settling state. Recorded 2026-08-24 (#427).", - "issue": "https://github.com/stephrobert/feint/issues/433" + "operation": "vpcgw/v2/API.GetGateway", + "kind": "absent", + "path": "message", + "reason": "THIS IS THE RECORDING, NOT THE PACK, and the recording proves it in its own sequence numbers. The gateway goes from 200 on its seventh read to 404 on its eighth WITH NO DELETE ANYWHERE IN THE FILE, and its address follows: the recorded DeleteIP answers 404 and the recorded GetIP answers 404 too. The destruction did not travel through the proxy. STATED RATHER THAN ASSUMED: corpus/scaleway/scw-billed-shapes.jsonl holds three recording sessions whose own sequence numbers run 1..14, 1..43 and 1..65 with no gap, so nothing was dropped between the request that read the gateway and the request that could not find it, and no DELETE on /vpc-gw/v2/.../gateways/ exists in that file at all. This emulator was never asked to delete the gateway, so it still answers it -- and its DeleteIP refuses with 400 because the address is still attached to a gateway nobody destroyed. Answering 404 for an object nobody destroyed would be the lie this project exists to avoid. The `absent` findings on these operations are the fields of the cloud's 404 body, which is the same cause seen from the body. It goes when the gateway is recorded again with its destruction in the transcript. Recorded 2026-08-24 (#427).", + "issue": "https://github.com/stephrobert/feint/issues/435" }, { "file": "scaleway/scw-billed-shapes.jsonl", - "operation": "block/v1alpha1/API.GetSnapshot", + "operation": "vpcgw/v2/API.GetGateway", "kind": "absent", - "path": "class", - "reason": "THIS EMULATOR'S LIFECYCLE TRANSITIONS ARE IMMEDIATE, which docs/limits.md states as a decision, and this is that decision seen from block/v1alpha1. A snapshot is `available` the instant it is cut here; upstream it is born in a transient state and a DeleteSnapshot issued while it settles is refused with 412 and a body. corpus/scaleway/scw-billed-shapes.jsonl seq 9-12 is exactly that sequence: the refused delete, a read that still finds the snapshot, the accepted delete, a read that does not. Here the FIRST delete succeeds, so the read between them finds nothing (404 against 200, which reports all twelve fields of the snapshot absent) and the second delete meets nothing (404 against 204). A refusal whose state is not reachable here cannot be served: a guard for a state nothing can enter is a control that can never fire, which is the rule docs/limits.md already draws for Outscale's InvalidVolumeState. It goes the day a snapshot has a settling state. Recorded 2026-08-24 (#427).", - "issue": "https://github.com/stephrobert/feint/issues/433" + "path": "resource", + "reason": "THIS IS THE RECORDING, NOT THE PACK, and the recording proves it in its own sequence numbers. The gateway goes from 200 on its seventh read to 404 on its eighth WITH NO DELETE ANYWHERE IN THE FILE, and its address follows: the recorded DeleteIP answers 404 and the recorded GetIP answers 404 too. The destruction did not travel through the proxy. STATED RATHER THAN ASSUMED: corpus/scaleway/scw-billed-shapes.jsonl holds three recording sessions whose own sequence numbers run 1..14, 1..43 and 1..65 with no gap, so nothing was dropped between the request that read the gateway and the request that could not find it, and no DELETE on /vpc-gw/v2/.../gateways/ exists in that file at all. This emulator was never asked to delete the gateway, so it still answers it -- and its DeleteIP refuses with 400 because the address is still attached to a gateway nobody destroyed. Answering 404 for an object nobody destroyed would be the lie this project exists to avoid. The `absent` findings on these operations are the fields of the cloud's 404 body, which is the same cause seen from the body. It goes when the gateway is recorded again with its destruction in the transcript. Recorded 2026-08-24 (#427).", + "issue": "https://github.com/stephrobert/feint/issues/435" }, { "file": "scaleway/scw-billed-shapes.jsonl", - "operation": "block/v1alpha1/API.GetSnapshot", + "operation": "vpcgw/v2/API.GetGateway", "kind": "absent", - "path": "created_at", - "reason": "THIS EMULATOR'S LIFECYCLE TRANSITIONS ARE IMMEDIATE, which docs/limits.md states as a decision, and this is that decision seen from block/v1alpha1. A snapshot is `available` the instant it is cut here; upstream it is born in a transient state and a DeleteSnapshot issued while it settles is refused with 412 and a body. corpus/scaleway/scw-billed-shapes.jsonl seq 9-12 is exactly that sequence: the refused delete, a read that still finds the snapshot, the accepted delete, a read that does not. Here the FIRST delete succeeds, so the read between them finds nothing (404 against 200, which reports all twelve fields of the snapshot absent) and the second delete meets nothing (404 against 204). A refusal whose state is not reachable here cannot be served: a guard for a state nothing can enter is a control that can never fire, which is the rule docs/limits.md already draws for Outscale's InvalidVolumeState. It goes the day a snapshot has a settling state. Recorded 2026-08-24 (#427).", - "issue": "https://github.com/stephrobert/feint/issues/433" + "path": "resource_id", + "reason": "THIS IS THE RECORDING, NOT THE PACK, and the recording proves it in its own sequence numbers. The gateway goes from 200 on its seventh read to 404 on its eighth WITH NO DELETE ANYWHERE IN THE FILE, and its address follows: the recorded DeleteIP answers 404 and the recorded GetIP answers 404 too. The destruction did not travel through the proxy. STATED RATHER THAN ASSUMED: corpus/scaleway/scw-billed-shapes.jsonl holds three recording sessions whose own sequence numbers run 1..14, 1..43 and 1..65 with no gap, so nothing was dropped between the request that read the gateway and the request that could not find it, and no DELETE on /vpc-gw/v2/.../gateways/ exists in that file at all. This emulator was never asked to delete the gateway, so it still answers it -- and its DeleteIP refuses with 400 because the address is still attached to a gateway nobody destroyed. Answering 404 for an object nobody destroyed would be the lie this project exists to avoid. The `absent` findings on these operations are the fields of the cloud's 404 body, which is the same cause seen from the body. It goes when the gateway is recorded again with its destruction in the transcript. Recorded 2026-08-24 (#427).", + "issue": "https://github.com/stephrobert/feint/issues/435" }, { "file": "scaleway/scw-billed-shapes.jsonl", - "operation": "block/v1alpha1/API.GetSnapshot", - "kind": "absent", - "path": "id", - "reason": "THIS EMULATOR'S LIFECYCLE TRANSITIONS ARE IMMEDIATE, which docs/limits.md states as a decision, and this is that decision seen from block/v1alpha1. A snapshot is `available` the instant it is cut here; upstream it is born in a transient state and a DeleteSnapshot issued while it settles is refused with 412 and a body. corpus/scaleway/scw-billed-shapes.jsonl seq 9-12 is exactly that sequence: the refused delete, a read that still finds the snapshot, the accepted delete, a read that does not. Here the FIRST delete succeeds, so the read between them finds nothing (404 against 200, which reports all twelve fields of the snapshot absent) and the second delete meets nothing (404 against 204). A refusal whose state is not reachable here cannot be served: a guard for a state nothing can enter is a control that can never fire, which is the rule docs/limits.md already draws for Outscale's InvalidVolumeState. It goes the day a snapshot has a settling state. Recorded 2026-08-24 (#427).", - "issue": "https://github.com/stephrobert/feint/issues/433" + "operation": "vpcgw/v2/API.GetGateway", + "kind": "status", + "path": "", + "reason": "THIS IS THE RECORDING, NOT THE PACK, and the recording proves it in its own sequence numbers. The gateway goes from 200 on its seventh read to 404 on its eighth WITH NO DELETE ANYWHERE IN THE FILE, and its address follows: the recorded DeleteIP answers 404 and the recorded GetIP answers 404 too. The destruction did not travel through the proxy. STATED RATHER THAN ASSUMED: corpus/scaleway/scw-billed-shapes.jsonl holds three recording sessions whose own sequence numbers run 1..14, 1..43 and 1..65 with no gap, so nothing was dropped between the request that read the gateway and the request that could not find it, and no DELETE on /vpc-gw/v2/.../gateways/ exists in that file at all. This emulator was never asked to delete the gateway, so it still answers it -- and its DeleteIP refuses with 400 because the address is still attached to a gateway nobody destroyed. Answering 404 for an object nobody destroyed would be the lie this project exists to avoid. The `absent` findings on these operations are the fields of the cloud's 404 body, which is the same cause seen from the body. It goes when the gateway is recorded again with its destruction in the transcript. Recorded 2026-08-24 (#427).", + "issue": "https://github.com/stephrobert/feint/issues/435" }, { "file": "scaleway/scw-billed-shapes.jsonl", - "operation": "block/v1alpha1/API.GetSnapshot", + "operation": "vpcgw/v2/API.GetIP", "kind": "absent", - "path": "name", - "reason": "THIS EMULATOR'S LIFECYCLE TRANSITIONS ARE IMMEDIATE, which docs/limits.md states as a decision, and this is that decision seen from block/v1alpha1. A snapshot is `available` the instant it is cut here; upstream it is born in a transient state and a DeleteSnapshot issued while it settles is refused with 412 and a body. corpus/scaleway/scw-billed-shapes.jsonl seq 9-12 is exactly that sequence: the refused delete, a read that still finds the snapshot, the accepted delete, a read that does not. Here the FIRST delete succeeds, so the read between them finds nothing (404 against 200, which reports all twelve fields of the snapshot absent) and the second delete meets nothing (404 against 204). A refusal whose state is not reachable here cannot be served: a guard for a state nothing can enter is a control that can never fire, which is the rule docs/limits.md already draws for Outscale's InvalidVolumeState. It goes the day a snapshot has a settling state. Recorded 2026-08-24 (#427).", - "issue": "https://github.com/stephrobert/feint/issues/433" + "path": "message", + "reason": "THIS IS THE RECORDING, NOT THE PACK, and the recording proves it in its own sequence numbers. The gateway goes from 200 on its seventh read to 404 on its eighth WITH NO DELETE ANYWHERE IN THE FILE, and its address follows: the recorded DeleteIP answers 404 and the recorded GetIP answers 404 too. The destruction did not travel through the proxy. STATED RATHER THAN ASSUMED: corpus/scaleway/scw-billed-shapes.jsonl holds three recording sessions whose own sequence numbers run 1..14, 1..43 and 1..65 with no gap, so nothing was dropped between the request that read the gateway and the request that could not find it, and no DELETE on /vpc-gw/v2/.../gateways/ exists in that file at all. This emulator was never asked to delete the gateway, so it still answers it -- and its DeleteIP refuses with 400 because the address is still attached to a gateway nobody destroyed. Answering 404 for an object nobody destroyed would be the lie this project exists to avoid. The `absent` findings on these operations are the fields of the cloud's 404 body, which is the same cause seen from the body. It goes when the gateway is recorded again with its destruction in the transcript. Recorded 2026-08-24 (#427).", + "issue": "https://github.com/stephrobert/feint/issues/435" }, { "file": "scaleway/scw-billed-shapes.jsonl", - "operation": "block/v1alpha1/API.GetSnapshot", + "operation": "vpcgw/v2/API.GetIP", "kind": "absent", - "path": "parent_volume", - "reason": "THIS EMULATOR'S LIFECYCLE TRANSITIONS ARE IMMEDIATE, which docs/limits.md states as a decision, and this is that decision seen from block/v1alpha1. A snapshot is `available` the instant it is cut here; upstream it is born in a transient state and a DeleteSnapshot issued while it settles is refused with 412 and a body. corpus/scaleway/scw-billed-shapes.jsonl seq 9-12 is exactly that sequence: the refused delete, a read that still finds the snapshot, the accepted delete, a read that does not. Here the FIRST delete succeeds, so the read between them finds nothing (404 against 200, which reports all twelve fields of the snapshot absent) and the second delete meets nothing (404 against 204). A refusal whose state is not reachable here cannot be served: a guard for a state nothing can enter is a control that can never fire, which is the rule docs/limits.md already draws for Outscale's InvalidVolumeState. It goes the day a snapshot has a settling state. Recorded 2026-08-24 (#427).", - "issue": "https://github.com/stephrobert/feint/issues/433" + "path": "resource", + "reason": "THIS IS THE RECORDING, NOT THE PACK, and the recording proves it in its own sequence numbers. The gateway goes from 200 on its seventh read to 404 on its eighth WITH NO DELETE ANYWHERE IN THE FILE, and its address follows: the recorded DeleteIP answers 404 and the recorded GetIP answers 404 too. The destruction did not travel through the proxy. STATED RATHER THAN ASSUMED: corpus/scaleway/scw-billed-shapes.jsonl holds three recording sessions whose own sequence numbers run 1..14, 1..43 and 1..65 with no gap, so nothing was dropped between the request that read the gateway and the request that could not find it, and no DELETE on /vpc-gw/v2/.../gateways/ exists in that file at all. This emulator was never asked to delete the gateway, so it still answers it -- and its DeleteIP refuses with 400 because the address is still attached to a gateway nobody destroyed. Answering 404 for an object nobody destroyed would be the lie this project exists to avoid. The `absent` findings on these operations are the fields of the cloud's 404 body, which is the same cause seen from the body. It goes when the gateway is recorded again with its destruction in the transcript. Recorded 2026-08-24 (#427).", + "issue": "https://github.com/stephrobert/feint/issues/435" }, { "file": "scaleway/scw-billed-shapes.jsonl", - "operation": "block/v1alpha1/API.GetSnapshot", + "operation": "vpcgw/v2/API.GetIP", "kind": "absent", - "path": "project_id", - "reason": "THIS EMULATOR'S LIFECYCLE TRANSITIONS ARE IMMEDIATE, which docs/limits.md states as a decision, and this is that decision seen from block/v1alpha1. A snapshot is `available` the instant it is cut here; upstream it is born in a transient state and a DeleteSnapshot issued while it settles is refused with 412 and a body. corpus/scaleway/scw-billed-shapes.jsonl seq 9-12 is exactly that sequence: the refused delete, a read that still finds the snapshot, the accepted delete, a read that does not. Here the FIRST delete succeeds, so the read between them finds nothing (404 against 200, which reports all twelve fields of the snapshot absent) and the second delete meets nothing (404 against 204). A refusal whose state is not reachable here cannot be served: a guard for a state nothing can enter is a control that can never fire, which is the rule docs/limits.md already draws for Outscale's InvalidVolumeState. It goes the day a snapshot has a settling state. Recorded 2026-08-24 (#427).", - "issue": "https://github.com/stephrobert/feint/issues/433" + "path": "resource_id", + "reason": "THIS IS THE RECORDING, NOT THE PACK, and the recording proves it in its own sequence numbers. The gateway goes from 200 on its seventh read to 404 on its eighth WITH NO DELETE ANYWHERE IN THE FILE, and its address follows: the recorded DeleteIP answers 404 and the recorded GetIP answers 404 too. The destruction did not travel through the proxy. STATED RATHER THAN ASSUMED: corpus/scaleway/scw-billed-shapes.jsonl holds three recording sessions whose own sequence numbers run 1..14, 1..43 and 1..65 with no gap, so nothing was dropped between the request that read the gateway and the request that could not find it, and no DELETE on /vpc-gw/v2/.../gateways/ exists in that file at all. This emulator was never asked to delete the gateway, so it still answers it -- and its DeleteIP refuses with 400 because the address is still attached to a gateway nobody destroyed. Answering 404 for an object nobody destroyed would be the lie this project exists to avoid. The `absent` findings on these operations are the fields of the cloud's 404 body, which is the same cause seen from the body. It goes when the gateway is recorded again with its destruction in the transcript. Recorded 2026-08-24 (#427).", + "issue": "https://github.com/stephrobert/feint/issues/435" }, { "file": "scaleway/scw-billed-shapes.jsonl", - "operation": "block/v1alpha1/API.GetSnapshot", + "operation": "vpcgw/v2/API.GetIP", "kind": "absent", - "path": "references", - "reason": "THIS EMULATOR'S LIFECYCLE TRANSITIONS ARE IMMEDIATE, which docs/limits.md states as a decision, and this is that decision seen from block/v1alpha1. A snapshot is `available` the instant it is cut here; upstream it is born in a transient state and a DeleteSnapshot issued while it settles is refused with 412 and a body. corpus/scaleway/scw-billed-shapes.jsonl seq 9-12 is exactly that sequence: the refused delete, a read that still finds the snapshot, the accepted delete, a read that does not. Here the FIRST delete succeeds, so the read between them finds nothing (404 against 200, which reports all twelve fields of the snapshot absent) and the second delete meets nothing (404 against 204). A refusal whose state is not reachable here cannot be served: a guard for a state nothing can enter is a control that can never fire, which is the rule docs/limits.md already draws for Outscale's InvalidVolumeState. It goes the day a snapshot has a settling state. Recorded 2026-08-24 (#427).", - "issue": "https://github.com/stephrobert/feint/issues/433" + "path": "type", + "reason": "THIS IS THE RECORDING, NOT THE PACK, and the recording proves it in its own sequence numbers. The gateway goes from 200 on its seventh read to 404 on its eighth WITH NO DELETE ANYWHERE IN THE FILE, and its address follows: the recorded DeleteIP answers 404 and the recorded GetIP answers 404 too. The destruction did not travel through the proxy. STATED RATHER THAN ASSUMED: corpus/scaleway/scw-billed-shapes.jsonl holds three recording sessions whose own sequence numbers run 1..14, 1..43 and 1..65 with no gap, so nothing was dropped between the request that read the gateway and the request that could not find it, and no DELETE on /vpc-gw/v2/.../gateways/ exists in that file at all. This emulator was never asked to delete the gateway, so it still answers it -- and its DeleteIP refuses with 400 because the address is still attached to a gateway nobody destroyed. Answering 404 for an object nobody destroyed would be the lie this project exists to avoid. The `absent` findings on these operations are the fields of the cloud's 404 body, which is the same cause seen from the body. It goes when the gateway is recorded again with its destruction in the transcript. Recorded 2026-08-24 (#427).", + "issue": "https://github.com/stephrobert/feint/issues/435" }, { "file": "scaleway/scw-billed-shapes.jsonl", - "operation": "block/v1alpha1/API.GetSnapshot", - "kind": "absent", - "path": "size", - "reason": "THIS EMULATOR'S LIFECYCLE TRANSITIONS ARE IMMEDIATE, which docs/limits.md states as a decision, and this is that decision seen from block/v1alpha1. A snapshot is `available` the instant it is cut here; upstream it is born in a transient state and a DeleteSnapshot issued while it settles is refused with 412 and a body. corpus/scaleway/scw-billed-shapes.jsonl seq 9-12 is exactly that sequence: the refused delete, a read that still finds the snapshot, the accepted delete, a read that does not. Here the FIRST delete succeeds, so the read between them finds nothing (404 against 200, which reports all twelve fields of the snapshot absent) and the second delete meets nothing (404 against 204). A refusal whose state is not reachable here cannot be served: a guard for a state nothing can enter is a control that can never fire, which is the rule docs/limits.md already draws for Outscale's InvalidVolumeState. It goes the day a snapshot has a settling state. Recorded 2026-08-24 (#427).", - "issue": "https://github.com/stephrobert/feint/issues/433" + "operation": "vpcgw/v2/API.GetIP", + "kind": "status", + "path": "", + "reason": "THIS IS THE RECORDING, NOT THE PACK, and the recording proves it in its own sequence numbers. The gateway goes from 200 on its seventh read to 404 on its eighth WITH NO DELETE ANYWHERE IN THE FILE, and its address follows: the recorded DeleteIP answers 404 and the recorded GetIP answers 404 too. The destruction did not travel through the proxy. STATED RATHER THAN ASSUMED: corpus/scaleway/scw-billed-shapes.jsonl holds three recording sessions whose own sequence numbers run 1..14, 1..43 and 1..65 with no gap, so nothing was dropped between the request that read the gateway and the request that could not find it, and no DELETE on /vpc-gw/v2/.../gateways/ exists in that file at all. This emulator was never asked to delete the gateway, so it still answers it -- and its DeleteIP refuses with 400 because the address is still attached to a gateway nobody destroyed. Answering 404 for an object nobody destroyed would be the lie this project exists to avoid. The `absent` findings on these operations are the fields of the cloud's 404 body, which is the same cause seen from the body. It goes when the gateway is recorded again with its destruction in the transcript. Recorded 2026-08-24 (#427).", + "issue": "https://github.com/stephrobert/feint/issues/435" }, { "file": "scaleway/scw-billed-shapes.jsonl", - "operation": "block/v1alpha1/API.GetSnapshot", + "operation": "vpcgw/v2/API.UpdateGateway", "kind": "absent", - "path": "status", - "reason": "THIS EMULATOR'S LIFECYCLE TRANSITIONS ARE IMMEDIATE, which docs/limits.md states as a decision, and this is that decision seen from block/v1alpha1. A snapshot is `available` the instant it is cut here; upstream it is born in a transient state and a DeleteSnapshot issued while it settles is refused with 412 and a body. corpus/scaleway/scw-billed-shapes.jsonl seq 9-12 is exactly that sequence: the refused delete, a read that still finds the snapshot, the accepted delete, a read that does not. Here the FIRST delete succeeds, so the read between them finds nothing (404 against 200, which reports all twelve fields of the snapshot absent) and the second delete meets nothing (404 against 204). A refusal whose state is not reachable here cannot be served: a guard for a state nothing can enter is a control that can never fire, which is the rule docs/limits.md already draws for Outscale's InvalidVolumeState. It goes the day a snapshot has a settling state. Recorded 2026-08-24 (#427).", - "issue": "https://github.com/stephrobert/feint/issues/433" + "path": "current_state", + "reason": "THIS EMULATOR'S LIFECYCLE TRANSITIONS ARE IMMEDIATE, and this is that decision seen from vpcgw/v2. A gateway is `running` the instant it is created here; upstream it is `allocating` for a few seconds, and both an UpdateGateway and a CreateGatewayNetwork issued in that window are refused with 409 and a body naming the state (current_state: allocating, plus message, resource, resource_id and type). corpus/scaleway/scw-billed-shapes.jsonl seq 40 and 41 are those two refusals, and seq 39 and 51-56 show the gateway moving from allocating to running around them. Both succeed here, so the status differs and the error body's own fields read as absent -- the `absent` findings on these two operations are the same cause seen from the body, not fields the pack fails to serve. Serving the refusal would mean inventing the window, and a guard for a state nothing can enter is a control that can never fire. It goes the day a gateway allocates. Recorded 2026-08-24 (#427).", + "issue": "https://github.com/stephrobert/feint/issues/435" }, { "file": "scaleway/scw-billed-shapes.jsonl", - "operation": "block/v1alpha1/API.GetSnapshot", + "operation": "vpcgw/v2/API.UpdateGateway", "kind": "absent", - "path": "tags", - "reason": "THIS EMULATOR'S LIFECYCLE TRANSITIONS ARE IMMEDIATE, which docs/limits.md states as a decision, and this is that decision seen from block/v1alpha1. A snapshot is `available` the instant it is cut here; upstream it is born in a transient state and a DeleteSnapshot issued while it settles is refused with 412 and a body. corpus/scaleway/scw-billed-shapes.jsonl seq 9-12 is exactly that sequence: the refused delete, a read that still finds the snapshot, the accepted delete, a read that does not. Here the FIRST delete succeeds, so the read between them finds nothing (404 against 200, which reports all twelve fields of the snapshot absent) and the second delete meets nothing (404 against 204). A refusal whose state is not reachable here cannot be served: a guard for a state nothing can enter is a control that can never fire, which is the rule docs/limits.md already draws for Outscale's InvalidVolumeState. It goes the day a snapshot has a settling state. Recorded 2026-08-24 (#427).", - "issue": "https://github.com/stephrobert/feint/issues/433" + "path": "message", + "reason": "THIS EMULATOR'S LIFECYCLE TRANSITIONS ARE IMMEDIATE, and this is that decision seen from vpcgw/v2. A gateway is `running` the instant it is created here; upstream it is `allocating` for a few seconds, and both an UpdateGateway and a CreateGatewayNetwork issued in that window are refused with 409 and a body naming the state (current_state: allocating, plus message, resource, resource_id and type). corpus/scaleway/scw-billed-shapes.jsonl seq 40 and 41 are those two refusals, and seq 39 and 51-56 show the gateway moving from allocating to running around them. Both succeed here, so the status differs and the error body's own fields read as absent -- the `absent` findings on these two operations are the same cause seen from the body, not fields the pack fails to serve. Serving the refusal would mean inventing the window, and a guard for a state nothing can enter is a control that can never fire. It goes the day a gateway allocates. Recorded 2026-08-24 (#427).", + "issue": "https://github.com/stephrobert/feint/issues/435" }, { "file": "scaleway/scw-billed-shapes.jsonl", - "operation": "block/v1alpha1/API.GetSnapshot", + "operation": "vpcgw/v2/API.UpdateGateway", "kind": "absent", - "path": "updated_at", - "reason": "THIS EMULATOR'S LIFECYCLE TRANSITIONS ARE IMMEDIATE, which docs/limits.md states as a decision, and this is that decision seen from block/v1alpha1. A snapshot is `available` the instant it is cut here; upstream it is born in a transient state and a DeleteSnapshot issued while it settles is refused with 412 and a body. corpus/scaleway/scw-billed-shapes.jsonl seq 9-12 is exactly that sequence: the refused delete, a read that still finds the snapshot, the accepted delete, a read that does not. Here the FIRST delete succeeds, so the read between them finds nothing (404 against 200, which reports all twelve fields of the snapshot absent) and the second delete meets nothing (404 against 204). A refusal whose state is not reachable here cannot be served: a guard for a state nothing can enter is a control that can never fire, which is the rule docs/limits.md already draws for Outscale's InvalidVolumeState. It goes the day a snapshot has a settling state. Recorded 2026-08-24 (#427).", - "issue": "https://github.com/stephrobert/feint/issues/433" + "path": "resource", + "reason": "THIS EMULATOR'S LIFECYCLE TRANSITIONS ARE IMMEDIATE, and this is that decision seen from vpcgw/v2. A gateway is `running` the instant it is created here; upstream it is `allocating` for a few seconds, and both an UpdateGateway and a CreateGatewayNetwork issued in that window are refused with 409 and a body naming the state (current_state: allocating, plus message, resource, resource_id and type). corpus/scaleway/scw-billed-shapes.jsonl seq 40 and 41 are those two refusals, and seq 39 and 51-56 show the gateway moving from allocating to running around them. Both succeed here, so the status differs and the error body's own fields read as absent -- the `absent` findings on these two operations are the same cause seen from the body, not fields the pack fails to serve. Serving the refusal would mean inventing the window, and a guard for a state nothing can enter is a control that can never fire. It goes the day a gateway allocates. Recorded 2026-08-24 (#427).", + "issue": "https://github.com/stephrobert/feint/issues/435" }, { "file": "scaleway/scw-billed-shapes.jsonl", - "operation": "block/v1alpha1/API.GetSnapshot", + "operation": "vpcgw/v2/API.UpdateGateway", "kind": "absent", - "path": "zone", - "reason": "THIS EMULATOR'S LIFECYCLE TRANSITIONS ARE IMMEDIATE, which docs/limits.md states as a decision, and this is that decision seen from block/v1alpha1. A snapshot is `available` the instant it is cut here; upstream it is born in a transient state and a DeleteSnapshot issued while it settles is refused with 412 and a body. corpus/scaleway/scw-billed-shapes.jsonl seq 9-12 is exactly that sequence: the refused delete, a read that still finds the snapshot, the accepted delete, a read that does not. Here the FIRST delete succeeds, so the read between them finds nothing (404 against 200, which reports all twelve fields of the snapshot absent) and the second delete meets nothing (404 against 204). A refusal whose state is not reachable here cannot be served: a guard for a state nothing can enter is a control that can never fire, which is the rule docs/limits.md already draws for Outscale's InvalidVolumeState. It goes the day a snapshot has a settling state. Recorded 2026-08-24 (#427).", - "issue": "https://github.com/stephrobert/feint/issues/433" + "path": "resource_id", + "reason": "THIS EMULATOR'S LIFECYCLE TRANSITIONS ARE IMMEDIATE, and this is that decision seen from vpcgw/v2. A gateway is `running` the instant it is created here; upstream it is `allocating` for a few seconds, and both an UpdateGateway and a CreateGatewayNetwork issued in that window are refused with 409 and a body naming the state (current_state: allocating, plus message, resource, resource_id and type). corpus/scaleway/scw-billed-shapes.jsonl seq 40 and 41 are those two refusals, and seq 39 and 51-56 show the gateway moving from allocating to running around them. Both succeed here, so the status differs and the error body's own fields read as absent -- the `absent` findings on these two operations are the same cause seen from the body, not fields the pack fails to serve. Serving the refusal would mean inventing the window, and a guard for a state nothing can enter is a control that can never fire. It goes the day a gateway allocates. Recorded 2026-08-24 (#427).", + "issue": "https://github.com/stephrobert/feint/issues/435" }, { "file": "scaleway/scw-billed-shapes.jsonl", - "operation": "block/v1alpha1/API.GetVolume", + "operation": "vpcgw/v2/API.UpdateGateway", "kind": "status", "path": "", - "reason": "A SERVER'S ROOT VOLUME LIVES IN instance/v1 HERE AND IN block UPSTREAM, and every finding on this operation is that one default. corpus/scaleway/scw-billed-shapes.jsonl seq 11 is a CreateServer whose body names no volume, and fr-par answered volumes {\"0\": {volume_type: sbs_volume}} -- a block volume -- then read it three times through block/v1alpha1 (seq 13, 27, 33) and deleted it there (seq 34). This emulator gives such a server a b_ssd volume in instance/v1, so all four calls answer 404, and a 404 reports every field of the expected body as absent. sbs_volume IS honoured when a client asks for it, and tools/conformance/scaleway/terraform/main.tf asks for it, so the path is proven end to end by the real provider; what is not done is the default. MEASURED WHY NOT: flipping it reds ten tests at once, because the whole instance/v1 volume surface reads a server's root disk out of the instance store -- CreateSnapshot and CreateImage cannot find the volume to snapshot, attach-volume and detach-volume refuse it, and terminate stops carrying it away. docs/limits.md carries it. It goes the day the default moves, which is a batch of its own and not a line in a handler. Recorded 2026-08-24 (#427).", - "issue": "https://github.com/stephrobert/feint/issues/433" + "reason": "THIS EMULATOR'S LIFECYCLE TRANSITIONS ARE IMMEDIATE, and this is that decision seen from vpcgw/v2. A gateway is `running` the instant it is created here; upstream it is `allocating` for a few seconds, and both an UpdateGateway and a CreateGatewayNetwork issued in that window are refused with 409 and a body naming the state (current_state: allocating, plus message, resource, resource_id and type). corpus/scaleway/scw-billed-shapes.jsonl seq 40 and 41 are those two refusals, and seq 39 and 51-56 show the gateway moving from allocating to running around them. Both succeed here, so the status differs and the error body's own fields read as absent -- the `absent` findings on these two operations are the same cause seen from the body, not fields the pack fails to serve. Serving the refusal would mean inventing the window, and a guard for a state nothing can enter is a control that can never fire. It goes the day a gateway allocates. Recorded 2026-08-24 (#427).", + "issue": "https://github.com/stephrobert/feint/issues/435" }, { - "file": "scaleway/scw-billed-shapes.jsonl", - "operation": "block/v1alpha1/API.GetVolume", + "file": "scaleway/scw-cli.jsonl", + "operation": "vpc/v2/API.CreatePrivateNetwork", "kind": "absent", - "path": "created_at", - "reason": "A SERVER'S ROOT VOLUME LIVES IN instance/v1 HERE AND IN block UPSTREAM, and every finding on this operation is that one default. corpus/scaleway/scw-billed-shapes.jsonl seq 11 is a CreateServer whose body names no volume, and fr-par answered volumes {\"0\": {volume_type: sbs_volume}} -- a block volume -- then read it three times through block/v1alpha1 (seq 13, 27, 33) and deleted it there (seq 34). This emulator gives such a server a b_ssd volume in instance/v1, so all four calls answer 404, and a 404 reports every field of the expected body as absent. sbs_volume IS honoured when a client asks for it, and tools/conformance/scaleway/terraform/main.tf asks for it, so the path is proven end to end by the real provider; what is not done is the default. MEASURED WHY NOT: flipping it reds ten tests at once, because the whole instance/v1 volume surface reads a server's root disk out of the instance store -- CreateSnapshot and CreateImage cannot find the volume to snapshot, attach-volume and detach-volume refuse it, and terminate stops carrying it away. docs/limits.md carries it. It goes the day the default moves, which is a batch of its own and not a line in a handler. Recorded 2026-08-24 (#427).", - "issue": "https://github.com/stephrobert/feint/issues/433" + "path": "has_s3_integration", + "reason": "THE RECORDING PREDATES THE RENAME, AND ONLY AN ACCOUNT CAN RE-RECORD IT. Scaleway renamed the vpc/v2 Object Storage family on 2026-08-25: the VPC's s3_integration_enabled became object_storage_private_access_enabled and the Private Network's has_s3_integration became has_object_storage_private_access. Both upstream sources agree and neither declares a deprecation alias, read on 2026-08-28: .upstream/scaleway-sdk-go/api/vpc/v2/vpc_sdk.go:1024 and :646, and .upstream/scaleway-openapi/vpc-v2.yml, whose x-properties-order -- the document's own exhaustive property list for each object -- carries the new name and not the old one. So the emulator answers the new name alone; answering both would invent a deprecation window no source declares. This corpus was recorded on 2026-08-20, five days before, so it is evidence of what the cloud answered BEFORE the rename and it still settles the two things a rename does not touch: the field is present on every answer, and its value is false on an account with nothing attached. Re-recording needs a paid vpc/v2 account this repository does not have, and `mise run corpus:cloud` is what would arbitrate it. It goes the day that recording is made. (#570)", + "issue": "https://github.com/stephrobert/feint/issues/570" }, { - "file": "scaleway/scw-billed-shapes.jsonl", - "operation": "block/v1alpha1/API.GetVolume", + "file": "scaleway/scw-cli.jsonl", + "operation": "vpc/v2/API.CreateVPC", "kind": "absent", - "path": "id", - "reason": "A SERVER'S ROOT VOLUME LIVES IN instance/v1 HERE AND IN block UPSTREAM, and every finding on this operation is that one default. corpus/scaleway/scw-billed-shapes.jsonl seq 11 is a CreateServer whose body names no volume, and fr-par answered volumes {\"0\": {volume_type: sbs_volume}} -- a block volume -- then read it three times through block/v1alpha1 (seq 13, 27, 33) and deleted it there (seq 34). This emulator gives such a server a b_ssd volume in instance/v1, so all four calls answer 404, and a 404 reports every field of the expected body as absent. sbs_volume IS honoured when a client asks for it, and tools/conformance/scaleway/terraform/main.tf asks for it, so the path is proven end to end by the real provider; what is not done is the default. MEASURED WHY NOT: flipping it reds ten tests at once, because the whole instance/v1 volume surface reads a server's root disk out of the instance store -- CreateSnapshot and CreateImage cannot find the volume to snapshot, attach-volume and detach-volume refuse it, and terminate stops carrying it away. docs/limits.md carries it. It goes the day the default moves, which is a batch of its own and not a line in a handler. Recorded 2026-08-24 (#427).", - "issue": "https://github.com/stephrobert/feint/issues/433" + "path": "s3_integration_enabled", + "reason": "THE RECORDING PREDATES THE RENAME, AND ONLY AN ACCOUNT CAN RE-RECORD IT. Scaleway renamed the vpc/v2 Object Storage family on 2026-08-25: the VPC's s3_integration_enabled became object_storage_private_access_enabled and the Private Network's has_s3_integration became has_object_storage_private_access. Both upstream sources agree and neither declares a deprecation alias, read on 2026-08-28: .upstream/scaleway-sdk-go/api/vpc/v2/vpc_sdk.go:1024 and :646, and .upstream/scaleway-openapi/vpc-v2.yml, whose x-properties-order -- the document's own exhaustive property list for each object -- carries the new name and not the old one. So the emulator answers the new name alone; answering both would invent a deprecation window no source declares. This corpus was recorded on 2026-08-20, five days before, so it is evidence of what the cloud answered BEFORE the rename and it still settles the two things a rename does not touch: the field is present on every answer, and its value is false on an account with nothing attached. Re-recording needs a paid vpc/v2 account this repository does not have, and `mise run corpus:cloud` is what would arbitrate it. It goes the day that recording is made. (#570)", + "issue": "https://github.com/stephrobert/feint/issues/570" }, { - "file": "scaleway/scw-billed-shapes.jsonl", - "operation": "block/v1alpha1/API.GetVolume", + "file": "scaleway/scw-cli.jsonl", + "operation": "vpc/v2/API.GetPrivateNetwork", "kind": "absent", - "path": "last_detached_at", - "reason": "A SERVER'S ROOT VOLUME LIVES IN instance/v1 HERE AND IN block UPSTREAM, and every finding on this operation is that one default. corpus/scaleway/scw-billed-shapes.jsonl seq 11 is a CreateServer whose body names no volume, and fr-par answered volumes {\"0\": {volume_type: sbs_volume}} -- a block volume -- then read it three times through block/v1alpha1 (seq 13, 27, 33) and deleted it there (seq 34). This emulator gives such a server a b_ssd volume in instance/v1, so all four calls answer 404, and a 404 reports every field of the expected body as absent. sbs_volume IS honoured when a client asks for it, and tools/conformance/scaleway/terraform/main.tf asks for it, so the path is proven end to end by the real provider; what is not done is the default. MEASURED WHY NOT: flipping it reds ten tests at once, because the whole instance/v1 volume surface reads a server's root disk out of the instance store -- CreateSnapshot and CreateImage cannot find the volume to snapshot, attach-volume and detach-volume refuse it, and terminate stops carrying it away. docs/limits.md carries it. It goes the day the default moves, which is a batch of its own and not a line in a handler. Recorded 2026-08-24 (#427).", - "issue": "https://github.com/stephrobert/feint/issues/433" + "path": "has_s3_integration", + "reason": "THE RECORDING PREDATES THE RENAME, AND ONLY AN ACCOUNT CAN RE-RECORD IT. Scaleway renamed the vpc/v2 Object Storage family on 2026-08-25: the VPC's s3_integration_enabled became object_storage_private_access_enabled and the Private Network's has_s3_integration became has_object_storage_private_access. Both upstream sources agree and neither declares a deprecation alias, read on 2026-08-28: .upstream/scaleway-sdk-go/api/vpc/v2/vpc_sdk.go:1024 and :646, and .upstream/scaleway-openapi/vpc-v2.yml, whose x-properties-order -- the document's own exhaustive property list for each object -- carries the new name and not the old one. So the emulator answers the new name alone; answering both would invent a deprecation window no source declares. This corpus was recorded on 2026-08-20, five days before, so it is evidence of what the cloud answered BEFORE the rename and it still settles the two things a rename does not touch: the field is present on every answer, and its value is false on an account with nothing attached. Re-recording needs a paid vpc/v2 account this repository does not have, and `mise run corpus:cloud` is what would arbitrate it. It goes the day that recording is made. (#570)", + "issue": "https://github.com/stephrobert/feint/issues/570" }, { - "file": "scaleway/scw-billed-shapes.jsonl", - "operation": "block/v1alpha1/API.GetVolume", + "file": "scaleway/scw-cli.jsonl", + "operation": "vpc/v2/API.GetVPC", "kind": "absent", - "path": "name", - "reason": "A SERVER'S ROOT VOLUME LIVES IN instance/v1 HERE AND IN block UPSTREAM, and every finding on this operation is that one default. corpus/scaleway/scw-billed-shapes.jsonl seq 11 is a CreateServer whose body names no volume, and fr-par answered volumes {\"0\": {volume_type: sbs_volume}} -- a block volume -- then read it three times through block/v1alpha1 (seq 13, 27, 33) and deleted it there (seq 34). This emulator gives such a server a b_ssd volume in instance/v1, so all four calls answer 404, and a 404 reports every field of the expected body as absent. sbs_volume IS honoured when a client asks for it, and tools/conformance/scaleway/terraform/main.tf asks for it, so the path is proven end to end by the real provider; what is not done is the default. MEASURED WHY NOT: flipping it reds ten tests at once, because the whole instance/v1 volume surface reads a server's root disk out of the instance store -- CreateSnapshot and CreateImage cannot find the volume to snapshot, attach-volume and detach-volume refuse it, and terminate stops carrying it away. docs/limits.md carries it. It goes the day the default moves, which is a batch of its own and not a line in a handler. Recorded 2026-08-24 (#427).", - "issue": "https://github.com/stephrobert/feint/issues/433" + "path": "s3_integration_enabled", + "reason": "THE RECORDING PREDATES THE RENAME, AND ONLY AN ACCOUNT CAN RE-RECORD IT. Scaleway renamed the vpc/v2 Object Storage family on 2026-08-25: the VPC's s3_integration_enabled became object_storage_private_access_enabled and the Private Network's has_s3_integration became has_object_storage_private_access. Both upstream sources agree and neither declares a deprecation alias, read on 2026-08-28: .upstream/scaleway-sdk-go/api/vpc/v2/vpc_sdk.go:1024 and :646, and .upstream/scaleway-openapi/vpc-v2.yml, whose x-properties-order -- the document's own exhaustive property list for each object -- carries the new name and not the old one. So the emulator answers the new name alone; answering both would invent a deprecation window no source declares. This corpus was recorded on 2026-08-20, five days before, so it is evidence of what the cloud answered BEFORE the rename and it still settles the two things a rename does not touch: the field is present on every answer, and its value is false on an account with nothing attached. Re-recording needs a paid vpc/v2 account this repository does not have, and `mise run corpus:cloud` is what would arbitrate it. It goes the day that recording is made. (#570)", + "issue": "https://github.com/stephrobert/feint/issues/570" }, { - "file": "scaleway/scw-billed-shapes.jsonl", - "operation": "block/v1alpha1/API.GetVolume", + "file": "scaleway/scw-cli.jsonl", + "operation": "vpc/v2/API.ListPrivateNetworks", "kind": "absent", - "path": "parent_snapshot_id", - "reason": "A SERVER'S ROOT VOLUME LIVES IN instance/v1 HERE AND IN block UPSTREAM, and every finding on this operation is that one default. corpus/scaleway/scw-billed-shapes.jsonl seq 11 is a CreateServer whose body names no volume, and fr-par answered volumes {\"0\": {volume_type: sbs_volume}} -- a block volume -- then read it three times through block/v1alpha1 (seq 13, 27, 33) and deleted it there (seq 34). This emulator gives such a server a b_ssd volume in instance/v1, so all four calls answer 404, and a 404 reports every field of the expected body as absent. sbs_volume IS honoured when a client asks for it, and tools/conformance/scaleway/terraform/main.tf asks for it, so the path is proven end to end by the real provider; what is not done is the default. MEASURED WHY NOT: flipping it reds ten tests at once, because the whole instance/v1 volume surface reads a server's root disk out of the instance store -- CreateSnapshot and CreateImage cannot find the volume to snapshot, attach-volume and detach-volume refuse it, and terminate stops carrying it away. docs/limits.md carries it. It goes the day the default moves, which is a batch of its own and not a line in a handler. Recorded 2026-08-24 (#427).", - "issue": "https://github.com/stephrobert/feint/issues/433" + "path": "private_networks[].has_s3_integration", + "reason": "THE RECORDING PREDATES THE RENAME, AND ONLY AN ACCOUNT CAN RE-RECORD IT. Scaleway renamed the vpc/v2 Object Storage family on 2026-08-25: the VPC's s3_integration_enabled became object_storage_private_access_enabled and the Private Network's has_s3_integration became has_object_storage_private_access. Both upstream sources agree and neither declares a deprecation alias, read on 2026-08-28: .upstream/scaleway-sdk-go/api/vpc/v2/vpc_sdk.go:1024 and :646, and .upstream/scaleway-openapi/vpc-v2.yml, whose x-properties-order -- the document's own exhaustive property list for each object -- carries the new name and not the old one. So the emulator answers the new name alone; answering both would invent a deprecation window no source declares. This corpus was recorded on 2026-08-20, five days before, so it is evidence of what the cloud answered BEFORE the rename and it still settles the two things a rename does not touch: the field is present on every answer, and its value is false on an account with nothing attached. Re-recording needs a paid vpc/v2 account this repository does not have, and `mise run corpus:cloud` is what would arbitrate it. It goes the day that recording is made. (#570)", + "issue": "https://github.com/stephrobert/feint/issues/570" }, { - "file": "scaleway/scw-billed-shapes.jsonl", - "operation": "block/v1alpha1/API.GetVolume", + "file": "scaleway/scw-cli.jsonl", + "operation": "vpc/v2/API.ListVPCs", "kind": "absent", - "path": "project_id", - "reason": "A SERVER'S ROOT VOLUME LIVES IN instance/v1 HERE AND IN block UPSTREAM, and every finding on this operation is that one default. corpus/scaleway/scw-billed-shapes.jsonl seq 11 is a CreateServer whose body names no volume, and fr-par answered volumes {\"0\": {volume_type: sbs_volume}} -- a block volume -- then read it three times through block/v1alpha1 (seq 13, 27, 33) and deleted it there (seq 34). This emulator gives such a server a b_ssd volume in instance/v1, so all four calls answer 404, and a 404 reports every field of the expected body as absent. sbs_volume IS honoured when a client asks for it, and tools/conformance/scaleway/terraform/main.tf asks for it, so the path is proven end to end by the real provider; what is not done is the default. MEASURED WHY NOT: flipping it reds ten tests at once, because the whole instance/v1 volume surface reads a server's root disk out of the instance store -- CreateSnapshot and CreateImage cannot find the volume to snapshot, attach-volume and detach-volume refuse it, and terminate stops carrying it away. docs/limits.md carries it. It goes the day the default moves, which is a batch of its own and not a line in a handler. Recorded 2026-08-24 (#427).", - "issue": "https://github.com/stephrobert/feint/issues/433" + "path": "vpcs[].s3_integration_enabled", + "reason": "THE RECORDING PREDATES THE RENAME, AND ONLY AN ACCOUNT CAN RE-RECORD IT. Scaleway renamed the vpc/v2 Object Storage family on 2026-08-25: the VPC's s3_integration_enabled became object_storage_private_access_enabled and the Private Network's has_s3_integration became has_object_storage_private_access. Both upstream sources agree and neither declares a deprecation alias, read on 2026-08-28: .upstream/scaleway-sdk-go/api/vpc/v2/vpc_sdk.go:1024 and :646, and .upstream/scaleway-openapi/vpc-v2.yml, whose x-properties-order -- the document's own exhaustive property list for each object -- carries the new name and not the old one. So the emulator answers the new name alone; answering both would invent a deprecation window no source declares. This corpus was recorded on 2026-08-20, five days before, so it is evidence of what the cloud answered BEFORE the rename and it still settles the two things a rename does not touch: the field is present on every answer, and its value is false on an account with nothing attached. Re-recording needs a paid vpc/v2 account this repository does not have, and `mise run corpus:cloud` is what would arbitrate it. It goes the day that recording is made. (#570)", + "issue": "https://github.com/stephrobert/feint/issues/570" }, { - "file": "scaleway/scw-billed-shapes.jsonl", - "operation": "block/v1alpha1/API.GetVolume", + "file": "scaleway/scw-cli.jsonl", + "operation": "vpc/v2/API.UpdatePrivateNetwork", "kind": "absent", - "path": "references", - "reason": "A SERVER'S ROOT VOLUME LIVES IN instance/v1 HERE AND IN block UPSTREAM, and every finding on this operation is that one default. corpus/scaleway/scw-billed-shapes.jsonl seq 11 is a CreateServer whose body names no volume, and fr-par answered volumes {\"0\": {volume_type: sbs_volume}} -- a block volume -- then read it three times through block/v1alpha1 (seq 13, 27, 33) and deleted it there (seq 34). This emulator gives such a server a b_ssd volume in instance/v1, so all four calls answer 404, and a 404 reports every field of the expected body as absent. sbs_volume IS honoured when a client asks for it, and tools/conformance/scaleway/terraform/main.tf asks for it, so the path is proven end to end by the real provider; what is not done is the default. MEASURED WHY NOT: flipping it reds ten tests at once, because the whole instance/v1 volume surface reads a server's root disk out of the instance store -- CreateSnapshot and CreateImage cannot find the volume to snapshot, attach-volume and detach-volume refuse it, and terminate stops carrying it away. docs/limits.md carries it. It goes the day the default moves, which is a batch of its own and not a line in a handler. Recorded 2026-08-24 (#427).", - "issue": "https://github.com/stephrobert/feint/issues/433" + "path": "has_s3_integration", + "reason": "THE RECORDING PREDATES THE RENAME, AND ONLY AN ACCOUNT CAN RE-RECORD IT. Scaleway renamed the vpc/v2 Object Storage family on 2026-08-25: the VPC's s3_integration_enabled became object_storage_private_access_enabled and the Private Network's has_s3_integration became has_object_storage_private_access. Both upstream sources agree and neither declares a deprecation alias, read on 2026-08-28: .upstream/scaleway-sdk-go/api/vpc/v2/vpc_sdk.go:1024 and :646, and .upstream/scaleway-openapi/vpc-v2.yml, whose x-properties-order -- the document's own exhaustive property list for each object -- carries the new name and not the old one. So the emulator answers the new name alone; answering both would invent a deprecation window no source declares. This corpus was recorded on 2026-08-20, five days before, so it is evidence of what the cloud answered BEFORE the rename and it still settles the two things a rename does not touch: the field is present on every answer, and its value is false on an account with nothing attached. Re-recording needs a paid vpc/v2 account this repository does not have, and `mise run corpus:cloud` is what would arbitrate it. It goes the day that recording is made. (#570)", + "issue": "https://github.com/stephrobert/feint/issues/570" }, { - "file": "scaleway/scw-billed-shapes.jsonl", - "operation": "block/v1alpha1/API.GetVolume", + "file": "scaleway/scw-cli.jsonl", + "operation": "vpc/v2/API.UpdateVPC", "kind": "absent", - "path": "size", - "reason": "A SERVER'S ROOT VOLUME LIVES IN instance/v1 HERE AND IN block UPSTREAM, and every finding on this operation is that one default. corpus/scaleway/scw-billed-shapes.jsonl seq 11 is a CreateServer whose body names no volume, and fr-par answered volumes {\"0\": {volume_type: sbs_volume}} -- a block volume -- then read it three times through block/v1alpha1 (seq 13, 27, 33) and deleted it there (seq 34). This emulator gives such a server a b_ssd volume in instance/v1, so all four calls answer 404, and a 404 reports every field of the expected body as absent. sbs_volume IS honoured when a client asks for it, and tools/conformance/scaleway/terraform/main.tf asks for it, so the path is proven end to end by the real provider; what is not done is the default. MEASURED WHY NOT: flipping it reds ten tests at once, because the whole instance/v1 volume surface reads a server's root disk out of the instance store -- CreateSnapshot and CreateImage cannot find the volume to snapshot, attach-volume and detach-volume refuse it, and terminate stops carrying it away. docs/limits.md carries it. It goes the day the default moves, which is a batch of its own and not a line in a handler. Recorded 2026-08-24 (#427).", - "issue": "https://github.com/stephrobert/feint/issues/433" + "path": "s3_integration_enabled", + "reason": "THE RECORDING PREDATES THE RENAME, AND ONLY AN ACCOUNT CAN RE-RECORD IT. Scaleway renamed the vpc/v2 Object Storage family on 2026-08-25: the VPC's s3_integration_enabled became object_storage_private_access_enabled and the Private Network's has_s3_integration became has_object_storage_private_access. Both upstream sources agree and neither declares a deprecation alias, read on 2026-08-28: .upstream/scaleway-sdk-go/api/vpc/v2/vpc_sdk.go:1024 and :646, and .upstream/scaleway-openapi/vpc-v2.yml, whose x-properties-order -- the document's own exhaustive property list for each object -- carries the new name and not the old one. So the emulator answers the new name alone; answering both would invent a deprecation window no source declares. This corpus was recorded on 2026-08-20, five days before, so it is evidence of what the cloud answered BEFORE the rename and it still settles the two things a rename does not touch: the field is present on every answer, and its value is false on an account with nothing attached. Re-recording needs a paid vpc/v2 account this repository does not have, and `mise run corpus:cloud` is what would arbitrate it. It goes the day that recording is made. (#570)", + "issue": "https://github.com/stephrobert/feint/issues/570" }, { - "file": "scaleway/scw-billed-shapes.jsonl", - "operation": "block/v1alpha1/API.GetVolume", + "operation": "instance/v1/API.ListSecurityGroupRules", "kind": "absent", - "path": "specs", - "reason": "A SERVER'S ROOT VOLUME LIVES IN instance/v1 HERE AND IN block UPSTREAM, and every finding on this operation is that one default. corpus/scaleway/scw-billed-shapes.jsonl seq 11 is a CreateServer whose body names no volume, and fr-par answered volumes {\"0\": {volume_type: sbs_volume}} -- a block volume -- then read it three times through block/v1alpha1 (seq 13, 27, 33) and deleted it there (seq 34). This emulator gives such a server a b_ssd volume in instance/v1, so all four calls answer 404, and a 404 reports every field of the expected body as absent. sbs_volume IS honoured when a client asks for it, and tools/conformance/scaleway/terraform/main.tf asks for it, so the path is proven end to end by the real provider; what is not done is the default. MEASURED WHY NOT: flipping it reds ten tests at once, because the whole instance/v1 volume surface reads a server's root disk out of the instance store -- CreateSnapshot and CreateImage cannot find the volume to snapshot, attach-volume and detach-volume refuse it, and terminate stops carrying it away. docs/limits.md carries it. It goes the day the default moves, which is a batch of its own and not a line in a handler. Recorded 2026-08-24 (#427).", - "issue": "https://github.com/stephrobert/feint/issues/433" + "path": "rules[]", + "reason": "The cloud folds its six account-wide default rules into every group's list, and this emulator does not. MEASURED, and the SDK settles what they are: instance_sdk.go documents ListDefaultSecurityGroupRules as \"Lists the default rules applied to all the security groups\", and corpus/scaleway/scw-free-shapes.jsonl seq 18 and seq 20 answer the SAME six rule identifiers for a group created seconds earlier and for the `default` segment -- they are one rule set, not a copy per group. This emulator now serves it at the door that names it (#432 closed that half), and deliberately does not fold it into a group: the six are outbound TCP drops on the submission ports, nothing here filters an outbound packet, and firewallSpecOf turns every rule of a group into a runtime ACL rule -- so folding them in would push a filter to Incus that the emulator would then be enforcing for real, and would offer a client six rules to delete that it never created. It goes the day the machine layer enforces the SMTP block, which is what would make the list true. Recorded 2026-08-24 against a real fr-par account (#427).", + "issue": "https://github.com/stephrobert/feint/issues/432", + "file": "scaleway/scw-free-shapes.jsonl" }, { - "file": "scaleway/scw-billed-shapes.jsonl", - "operation": "block/v1alpha1/API.GetVolume", + "operation": "ipam/v1/API.ListIPs", "kind": "absent", - "path": "status", - "reason": "A SERVER'S ROOT VOLUME LIVES IN instance/v1 HERE AND IN block UPSTREAM, and every finding on this operation is that one default. corpus/scaleway/scw-billed-shapes.jsonl seq 11 is a CreateServer whose body names no volume, and fr-par answered volumes {\"0\": {volume_type: sbs_volume}} -- a block volume -- then read it three times through block/v1alpha1 (seq 13, 27, 33) and deleted it there (seq 34). This emulator gives such a server a b_ssd volume in instance/v1, so all four calls answer 404, and a 404 reports every field of the expected body as absent. sbs_volume IS honoured when a client asks for it, and tools/conformance/scaleway/terraform/main.tf asks for it, so the path is proven end to end by the real provider; what is not done is the default. MEASURED WHY NOT: flipping it reds ten tests at once, because the whole instance/v1 volume surface reads a server's root disk out of the instance store -- CreateSnapshot and CreateImage cannot find the volume to snapshot, attach-volume and detach-volume refuse it, and terminate stops carrying it away. docs/limits.md carries it. It goes the day the default moves, which is a batch of its own and not a line in a handler. Recorded 2026-08-24 (#427).", - "issue": "https://github.com/stephrobert/feint/issues/433" + "path": "ips[]", + "reason": "The recorded BookIP names a project and the recorded ListIPs names none, so the cloud answered one address and this emulator answers an empty list. MEASURED RATHER THAN ASSUMED, and the first hypothesis was wrong: this is not an artefact of the sanitiser. Asked directly, the emulator's ListIPs?project_id= answers the address correctly, and only the UNFILTERED list is empty -- it scopes to the default project while the create honoured the project the request named. That is exactly the cause #369 already classifies for instance/v1 createServer and listServers, one product out. It is recorded here against ipam/v1 rather than folded into #369's Scaleway entries because the key must name the operation it excuses. Recorded 2026-08-24 against a real fr-par account (#427).", + "issue": "https://github.com/stephrobert/feint/issues/369", + "file": "scaleway/scw-free-shapes.jsonl" }, { - "file": "scaleway/scw-billed-shapes.jsonl", - "operation": "block/v1alpha1/API.GetVolume", + "file": "scaleway/scw-free-shapes.jsonl", + "operation": "vpc/v2/API.CreatePrivateNetwork", "kind": "absent", - "path": "tags", - "reason": "A SERVER'S ROOT VOLUME LIVES IN instance/v1 HERE AND IN block UPSTREAM, and every finding on this operation is that one default. corpus/scaleway/scw-billed-shapes.jsonl seq 11 is a CreateServer whose body names no volume, and fr-par answered volumes {\"0\": {volume_type: sbs_volume}} -- a block volume -- then read it three times through block/v1alpha1 (seq 13, 27, 33) and deleted it there (seq 34). This emulator gives such a server a b_ssd volume in instance/v1, so all four calls answer 404, and a 404 reports every field of the expected body as absent. sbs_volume IS honoured when a client asks for it, and tools/conformance/scaleway/terraform/main.tf asks for it, so the path is proven end to end by the real provider; what is not done is the default. MEASURED WHY NOT: flipping it reds ten tests at once, because the whole instance/v1 volume surface reads a server's root disk out of the instance store -- CreateSnapshot and CreateImage cannot find the volume to snapshot, attach-volume and detach-volume refuse it, and terminate stops carrying it away. docs/limits.md carries it. It goes the day the default moves, which is a batch of its own and not a line in a handler. Recorded 2026-08-24 (#427).", - "issue": "https://github.com/stephrobert/feint/issues/433" + "path": "has_s3_integration", + "reason": "THE RECORDING PREDATES THE RENAME, AND ONLY AN ACCOUNT CAN RE-RECORD IT. Scaleway renamed the vpc/v2 Object Storage family on 2026-08-25: the VPC's s3_integration_enabled became object_storage_private_access_enabled and the Private Network's has_s3_integration became has_object_storage_private_access. Both upstream sources agree and neither declares a deprecation alias, read on 2026-08-28: .upstream/scaleway-sdk-go/api/vpc/v2/vpc_sdk.go:1024 and :646, and .upstream/scaleway-openapi/vpc-v2.yml, whose x-properties-order -- the document's own exhaustive property list for each object -- carries the new name and not the old one. So the emulator answers the new name alone; answering both would invent a deprecation window no source declares. This corpus was recorded on 2026-08-20, five days before, so it is evidence of what the cloud answered BEFORE the rename and it still settles the two things a rename does not touch: the field is present on every answer, and its value is false on an account with nothing attached. Re-recording needs a paid vpc/v2 account this repository does not have, and `mise run corpus:cloud` is what would arbitrate it. It goes the day that recording is made. (#570)", + "issue": "https://github.com/stephrobert/feint/issues/570" }, { - "file": "scaleway/scw-billed-shapes.jsonl", - "operation": "block/v1alpha1/API.GetVolume", + "file": "scaleway/scw-free-shapes.jsonl", + "operation": "vpc/v2/API.CreateVPC", "kind": "absent", - "path": "updated_at", - "reason": "A SERVER'S ROOT VOLUME LIVES IN instance/v1 HERE AND IN block UPSTREAM, and every finding on this operation is that one default. corpus/scaleway/scw-billed-shapes.jsonl seq 11 is a CreateServer whose body names no volume, and fr-par answered volumes {\"0\": {volume_type: sbs_volume}} -- a block volume -- then read it three times through block/v1alpha1 (seq 13, 27, 33) and deleted it there (seq 34). This emulator gives such a server a b_ssd volume in instance/v1, so all four calls answer 404, and a 404 reports every field of the expected body as absent. sbs_volume IS honoured when a client asks for it, and tools/conformance/scaleway/terraform/main.tf asks for it, so the path is proven end to end by the real provider; what is not done is the default. MEASURED WHY NOT: flipping it reds ten tests at once, because the whole instance/v1 volume surface reads a server's root disk out of the instance store -- CreateSnapshot and CreateImage cannot find the volume to snapshot, attach-volume and detach-volume refuse it, and terminate stops carrying it away. docs/limits.md carries it. It goes the day the default moves, which is a batch of its own and not a line in a handler. Recorded 2026-08-24 (#427).", - "issue": "https://github.com/stephrobert/feint/issues/433" + "path": "s3_integration_enabled", + "reason": "THE RECORDING PREDATES THE RENAME, AND ONLY AN ACCOUNT CAN RE-RECORD IT. Scaleway renamed the vpc/v2 Object Storage family on 2026-08-25: the VPC's s3_integration_enabled became object_storage_private_access_enabled and the Private Network's has_s3_integration became has_object_storage_private_access. Both upstream sources agree and neither declares a deprecation alias, read on 2026-08-28: .upstream/scaleway-sdk-go/api/vpc/v2/vpc_sdk.go:1024 and :646, and .upstream/scaleway-openapi/vpc-v2.yml, whose x-properties-order -- the document's own exhaustive property list for each object -- carries the new name and not the old one. So the emulator answers the new name alone; answering both would invent a deprecation window no source declares. This corpus was recorded on 2026-08-20, five days before, so it is evidence of what the cloud answered BEFORE the rename and it still settles the two things a rename does not touch: the field is present on every answer, and its value is false on an account with nothing attached. Re-recording needs a paid vpc/v2 account this repository does not have, and `mise run corpus:cloud` is what would arbitrate it. It goes the day that recording is made. (#570)", + "issue": "https://github.com/stephrobert/feint/issues/570" }, { - "file": "scaleway/scw-billed-shapes.jsonl", - "operation": "block/v1alpha1/API.GetVolume", + "file": "scaleway/scw-free-shapes.jsonl", + "operation": "vpc/v2/API.EnableDHCP", "kind": "absent", - "path": "zone", - "reason": "A SERVER'S ROOT VOLUME LIVES IN instance/v1 HERE AND IN block UPSTREAM, and every finding on this operation is that one default. corpus/scaleway/scw-billed-shapes.jsonl seq 11 is a CreateServer whose body names no volume, and fr-par answered volumes {\"0\": {volume_type: sbs_volume}} -- a block volume -- then read it three times through block/v1alpha1 (seq 13, 27, 33) and deleted it there (seq 34). This emulator gives such a server a b_ssd volume in instance/v1, so all four calls answer 404, and a 404 reports every field of the expected body as absent. sbs_volume IS honoured when a client asks for it, and tools/conformance/scaleway/terraform/main.tf asks for it, so the path is proven end to end by the real provider; what is not done is the default. MEASURED WHY NOT: flipping it reds ten tests at once, because the whole instance/v1 volume surface reads a server's root disk out of the instance store -- CreateSnapshot and CreateImage cannot find the volume to snapshot, attach-volume and detach-volume refuse it, and terminate stops carrying it away. docs/limits.md carries it. It goes the day the default moves, which is a batch of its own and not a line in a handler. Recorded 2026-08-24 (#427).", - "issue": "https://github.com/stephrobert/feint/issues/433" + "path": "has_s3_integration", + "reason": "THE RECORDING PREDATES THE RENAME, AND ONLY AN ACCOUNT CAN RE-RECORD IT. Scaleway renamed the vpc/v2 Object Storage family on 2026-08-25: the VPC's s3_integration_enabled became object_storage_private_access_enabled and the Private Network's has_s3_integration became has_object_storage_private_access. Both upstream sources agree and neither declares a deprecation alias, read on 2026-08-28: .upstream/scaleway-sdk-go/api/vpc/v2/vpc_sdk.go:1024 and :646, and .upstream/scaleway-openapi/vpc-v2.yml, whose x-properties-order -- the document's own exhaustive property list for each object -- carries the new name and not the old one. So the emulator answers the new name alone; answering both would invent a deprecation window no source declares. This corpus was recorded on 2026-08-20, five days before, so it is evidence of what the cloud answered BEFORE the rename and it still settles the two things a rename does not touch: the field is present on every answer, and its value is false on an account with nothing attached. Re-recording needs a paid vpc/v2 account this repository does not have, and `mise run corpus:cloud` is what would arbitrate it. It goes the day that recording is made. (#570)", + "issue": "https://github.com/stephrobert/feint/issues/570" }, { - "file": "scaleway/scw-billed-shapes.jsonl", + "file": "scaleway/scw-free-shapes.jsonl", + "operation": "vpc/v2/API.EnableRouting", + "kind": "absent", + "path": "s3_integration_enabled", + "reason": "THE RECORDING PREDATES THE RENAME, AND ONLY AN ACCOUNT CAN RE-RECORD IT. Scaleway renamed the vpc/v2 Object Storage family on 2026-08-25: the VPC's s3_integration_enabled became object_storage_private_access_enabled and the Private Network's has_s3_integration became has_object_storage_private_access. Both upstream sources agree and neither declares a deprecation alias, read on 2026-08-28: .upstream/scaleway-sdk-go/api/vpc/v2/vpc_sdk.go:1024 and :646, and .upstream/scaleway-openapi/vpc-v2.yml, whose x-properties-order -- the document's own exhaustive property list for each object -- carries the new name and not the old one. So the emulator answers the new name alone; answering both would invent a deprecation window no source declares. This corpus was recorded on 2026-08-20, five days before, so it is evidence of what the cloud answered BEFORE the rename and it still settles the two things a rename does not touch: the field is present on every answer, and its value is false on an account with nothing attached. Re-recording needs a paid vpc/v2 account this repository does not have, and `mise run corpus:cloud` is what would arbitrate it. It goes the day that recording is made. (#570)", + "issue": "https://github.com/stephrobert/feint/issues/570" + }, + { + "file": "scaleway/scw-instance.jsonl", "operation": "block/v1alpha1/API.DeleteVolume", "kind": "status", "path": "", - "reason": "A SERVER'S ROOT VOLUME LIVES IN instance/v1 HERE AND IN block UPSTREAM, and every finding on this operation is that one default. corpus/scaleway/scw-billed-shapes.jsonl seq 11 is a CreateServer whose body names no volume, and fr-par answered volumes {\"0\": {volume_type: sbs_volume}} -- a block volume -- then read it three times through block/v1alpha1 (seq 13, 27, 33) and deleted it there (seq 34). This emulator gives such a server a b_ssd volume in instance/v1, so all four calls answer 404, and a 404 reports every field of the expected body as absent. sbs_volume IS honoured when a client asks for it, and tools/conformance/scaleway/terraform/main.tf asks for it, so the path is proven end to end by the real provider; what is not done is the default. MEASURED WHY NOT: flipping it reds ten tests at once, because the whole instance/v1 volume surface reads a server's root disk out of the instance store -- CreateSnapshot and CreateImage cannot find the volume to snapshot, attach-volume and detach-volume refuse it, and terminate stops carrying it away. docs/limits.md carries it. It goes the day the default moves, which is a batch of its own and not a line in a handler. Recorded 2026-08-24 (#427).", - "issue": "https://github.com/stephrobert/feint/issues/433" - }, - { - "file": "scaleway/scw-billed-shapes.jsonl", - "operation": "lb/v1/ZonedAPI.GetLB", - "kind": "absent", - "path": "backend_count", - "reason": "THIS EMULATOR'S LIFECYCLE TRANSITIONS ARE IMMEDIATE, and this is that decision seen from lb/v1. A DeleteLB removes the balancer at once here; upstream the read that FOLLOWS the delete still answers 200 with status `to_delete`, and only the read after that answers 404. corpus/scaleway/scw-billed-shapes.jsonl seq 47-50 is the delete, one 200, then two 404s. So one read of the recording meets a 404 here where the cloud answered a whole balancer, and a 404 reports every field of that body as absent -- nineteen findings for one instant. Reproducing it means giving a delete a terminal state that a later read clears, which is a change to the lifecycle of every kind and to what `nothing left behind` means for every conformance suite -- the same argument #380 records for Outscale, one cloud out. It goes the day a delete is asynchronous here. Recorded 2026-08-24 (#427).", - "issue": "https://github.com/stephrobert/feint/issues/434" + "reason": "scw deletes the root volume through block/v1alpha1 after deleting the server, and the cloud answers 204 where this emulator answers 404 because the volume it created is an instance/v1 one", + "issue": "#365" }, { - "file": "scaleway/scw-billed-shapes.jsonl", - "operation": "lb/v1/ZonedAPI.GetLB", + "file": "scaleway/scw-instance.jsonl", + "operation": "block/v1alpha1/API.GetVolume", "kind": "absent", - "path": "created_at", - "reason": "THIS EMULATOR'S LIFECYCLE TRANSITIONS ARE IMMEDIATE, and this is that decision seen from lb/v1. A DeleteLB removes the balancer at once here; upstream the read that FOLLOWS the delete still answers 200 with status `to_delete`, and only the read after that answers 404. corpus/scaleway/scw-billed-shapes.jsonl seq 47-50 is the delete, one 200, then two 404s. So one read of the recording meets a 404 here where the cloud answered a whole balancer, and a 404 reports every field of that body as absent -- nineteen findings for one instant. Reproducing it means giving a delete a terminal state that a later read clears, which is a change to the lifecycle of every kind and to what `nothing left behind` means for every conformance suite -- the same argument #380 records for Outscale, one cloud out. It goes the day a delete is asynchronous here. Recorded 2026-08-24 (#427).", - "issue": "https://github.com/stephrobert/feint/issues/434" + "path": "*", + "reason": "every top-level field of a body this emulator does not serve at all: the block volume behind the root disk does not exist here, so its fourteen fields are absent for the one reason the status divergence above states", + "issue": "#365" }, { - "file": "scaleway/scw-billed-shapes.jsonl", - "operation": "lb/v1/ZonedAPI.GetLB", - "kind": "absent", - "path": "description", - "reason": "THIS EMULATOR'S LIFECYCLE TRANSITIONS ARE IMMEDIATE, and this is that decision seen from lb/v1. A DeleteLB removes the balancer at once here; upstream the read that FOLLOWS the delete still answers 200 with status `to_delete`, and only the read after that answers 404. corpus/scaleway/scw-billed-shapes.jsonl seq 47-50 is the delete, one 200, then two 404s. So one read of the recording meets a 404 here where the cloud answered a whole balancer, and a 404 reports every field of that body as absent -- nineteen findings for one instant. Reproducing it means giving a delete a terminal state that a later read clears, which is a change to the lifecycle of every kind and to what `nothing left behind` means for every conformance suite -- the same argument #380 records for Outscale, one cloud out. It goes the day a delete is asynchronous here. Recorded 2026-08-24 (#427).", - "issue": "https://github.com/stephrobert/feint/issues/434" + "file": "scaleway/scw-instance.jsonl", + "operation": "block/v1alpha1/API.GetVolume", + "kind": "status", + "path": "", + "reason": "the cloud gave the DEV1-S an SBS root volume and answers 200 on the block path scw follows; this emulator attaches a local instance/v1 volume, so the same read answers 404", + "issue": "#365" }, { - "file": "scaleway/scw-billed-shapes.jsonl", - "operation": "lb/v1/ZonedAPI.GetLB", + "file": "scaleway/scw-instance.jsonl", + "operation": "instance/v1/API.ListServers", "kind": "absent", - "path": "frontend_count", - "reason": "THIS EMULATOR'S LIFECYCLE TRANSITIONS ARE IMMEDIATE, and this is that decision seen from lb/v1. A DeleteLB removes the balancer at once here; upstream the read that FOLLOWS the delete still answers 200 with status `to_delete`, and only the read after that answers 404. corpus/scaleway/scw-billed-shapes.jsonl seq 47-50 is the delete, one 200, then two 404s. So one read of the recording meets a 404 here where the cloud answered a whole balancer, and a 404 reports every field of that body as absent -- nineteen findings for one instant. Reproducing it means giving a delete a terminal state that a later read clears, which is a change to the lifecycle of every kind and to what `nothing left behind` means for every conformance suite -- the same argument #380 records for Outscale, one cloud out. It goes the day a delete is asynchronous here. Recorded 2026-08-24 (#427).", - "issue": "https://github.com/stephrobert/feint/issues/434" + "path": "servers[]", + "reason": "the create honours the project the request named and the unfiltered list is scoped to the default project, so the server the cloud listed is invisible to the list that follows it here", + "issue": "#369" }, { - "file": "scaleway/scw-billed-shapes.jsonl", - "operation": "lb/v1/ZonedAPI.GetLB", + "file": "scaleway/scw-refusals.jsonl", + "operation": "instance/v1/API.CreateIP", "kind": "absent", - "path": "id", - "reason": "THIS EMULATOR'S LIFECYCLE TRANSITIONS ARE IMMEDIATE, and this is that decision seen from lb/v1. A DeleteLB removes the balancer at once here; upstream the read that FOLLOWS the delete still answers 200 with status `to_delete`, and only the read after that answers 404. corpus/scaleway/scw-billed-shapes.jsonl seq 47-50 is the delete, one 200, then two 404s. So one read of the recording meets a 404 here where the cloud answered a whole balancer, and a 404 reports every field of that body as absent -- nineteen findings for one instant. Reproducing it means giving a delete a terminal state that a later read clears, which is a change to the lifecycle of every kind and to what `nothing left behind` means for every conformance suite -- the same argument #380 records for Outscale, one cloud out. It goes the day a delete is asynchronous here. Recorded 2026-08-24 (#427).", - "issue": "https://github.com/stephrobert/feint/issues/434" + "path": "details", + "reason": "The create names a project identifier that does not exist and the cloud refuses it before allocating anything; this emulator accepts any project as an isolation boundary (projectOf, scopeOf) and creates the resource. Reproducing the refusal means the emulator must declare which project exists, which removes the multi-project behaviour it was built with, so it is a product decision rather than a patch. Recorded 2026-08-21 against a real fr-par account (#390).", + "issue": "https://github.com/stephrobert/feint/issues/391" }, { - "file": "scaleway/scw-billed-shapes.jsonl", - "operation": "lb/v1/ZonedAPI.GetLB", + "file": "scaleway/scw-refusals.jsonl", + "operation": "instance/v1/API.CreateIP", "kind": "absent", - "path": "instances", - "reason": "THIS EMULATOR'S LIFECYCLE TRANSITIONS ARE IMMEDIATE, and this is that decision seen from lb/v1. A DeleteLB removes the balancer at once here; upstream the read that FOLLOWS the delete still answers 200 with status `to_delete`, and only the read after that answers 404. corpus/scaleway/scw-billed-shapes.jsonl seq 47-50 is the delete, one 200, then two 404s. So one read of the recording meets a 404 here where the cloud answered a whole balancer, and a 404 reports every field of that body as absent -- nineteen findings for one instant. Reproducing it means giving a delete a terminal state that a later read clears, which is a change to the lifecycle of every kind and to what `nothing left behind` means for every conformance suite -- the same argument #380 records for Outscale, one cloud out. It goes the day a delete is asynchronous here. Recorded 2026-08-24 (#427).", - "issue": "https://github.com/stephrobert/feint/issues/434" + "path": "message", + "reason": "The create names a project identifier that does not exist and the cloud refuses it before allocating anything; this emulator accepts any project as an isolation boundary (projectOf, scopeOf) and creates the resource. Reproducing the refusal means the emulator must declare which project exists, which removes the multi-project behaviour it was built with, so it is a product decision rather than a patch. Recorded 2026-08-21 against a real fr-par account (#390).", + "issue": "https://github.com/stephrobert/feint/issues/391" }, { - "file": "scaleway/scw-billed-shapes.jsonl", - "operation": "lb/v1/ZonedAPI.GetLB", + "file": "scaleway/scw-refusals.jsonl", + "operation": "instance/v1/API.CreateIP", "kind": "absent", - "path": "ip", - "reason": "THIS EMULATOR'S LIFECYCLE TRANSITIONS ARE IMMEDIATE, and this is that decision seen from lb/v1. A DeleteLB removes the balancer at once here; upstream the read that FOLLOWS the delete still answers 200 with status `to_delete`, and only the read after that answers 404. corpus/scaleway/scw-billed-shapes.jsonl seq 47-50 is the delete, one 200, then two 404s. So one read of the recording meets a 404 here where the cloud answered a whole balancer, and a 404 reports every field of that body as absent -- nineteen findings for one instant. Reproducing it means giving a delete a terminal state that a later read clears, which is a change to the lifecycle of every kind and to what `nothing left behind` means for every conformance suite -- the same argument #380 records for Outscale, one cloud out. It goes the day a delete is asynchronous here. Recorded 2026-08-24 (#427).", - "issue": "https://github.com/stephrobert/feint/issues/434" + "path": "type", + "reason": "The create names a project identifier that does not exist and the cloud refuses it before allocating anything; this emulator accepts any project as an isolation boundary (projectOf, scopeOf) and creates the resource. Reproducing the refusal means the emulator must declare which project exists, which removes the multi-project behaviour it was built with, so it is a product decision rather than a patch. Recorded 2026-08-21 against a real fr-par account (#390).", + "issue": "https://github.com/stephrobert/feint/issues/391" }, { - "file": "scaleway/scw-billed-shapes.jsonl", - "operation": "lb/v1/ZonedAPI.GetLB", - "kind": "absent", - "path": "name", - "reason": "THIS EMULATOR'S LIFECYCLE TRANSITIONS ARE IMMEDIATE, and this is that decision seen from lb/v1. A DeleteLB removes the balancer at once here; upstream the read that FOLLOWS the delete still answers 200 with status `to_delete`, and only the read after that answers 404. corpus/scaleway/scw-billed-shapes.jsonl seq 47-50 is the delete, one 200, then two 404s. So one read of the recording meets a 404 here where the cloud answered a whole balancer, and a 404 reports every field of that body as absent -- nineteen findings for one instant. Reproducing it means giving a delete a terminal state that a later read clears, which is a change to the lifecycle of every kind and to what `nothing left behind` means for every conformance suite -- the same argument #380 records for Outscale, one cloud out. It goes the day a delete is asynchronous here. Recorded 2026-08-24 (#427).", - "issue": "https://github.com/stephrobert/feint/issues/434" + "file": "scaleway/scw-refusals.jsonl", + "operation": "instance/v1/API.CreateIP", + "kind": "status", + "path": "", + "reason": "The create names a project identifier that does not exist and the cloud refuses it before allocating anything; this emulator accepts any project as an isolation boundary (projectOf, scopeOf) and creates the resource. Reproducing the refusal means the emulator must declare which project exists, which removes the multi-project behaviour it was built with, so it is a product decision rather than a patch. Recorded 2026-08-21 against a real fr-par account (#390).", + "issue": "https://github.com/stephrobert/feint/issues/391" }, { - "file": "scaleway/scw-billed-shapes.jsonl", - "operation": "lb/v1/ZonedAPI.GetLB", + "file": "scaleway/scw-refusals.jsonl", + "operation": "instance/v1/API.CreatePlacementGroup", "kind": "absent", - "path": "organization_id", - "reason": "THIS EMULATOR'S LIFECYCLE TRANSITIONS ARE IMMEDIATE, and this is that decision seen from lb/v1. A DeleteLB removes the balancer at once here; upstream the read that FOLLOWS the delete still answers 200 with status `to_delete`, and only the read after that answers 404. corpus/scaleway/scw-billed-shapes.jsonl seq 47-50 is the delete, one 200, then two 404s. So one read of the recording meets a 404 here where the cloud answered a whole balancer, and a 404 reports every field of that body as absent -- nineteen findings for one instant. Reproducing it means giving a delete a terminal state that a later read clears, which is a change to the lifecycle of every kind and to what `nothing left behind` means for every conformance suite -- the same argument #380 records for Outscale, one cloud out. It goes the day a delete is asynchronous here. Recorded 2026-08-24 (#427).", - "issue": "https://github.com/stephrobert/feint/issues/434" + "path": "details", + "reason": "The create names a project identifier that does not exist and the cloud refuses it before allocating anything; this emulator accepts any project as an isolation boundary (projectOf, scopeOf) and creates the resource. Reproducing the refusal means the emulator must declare which project exists, which removes the multi-project behaviour it was built with, so it is a product decision rather than a patch. Recorded 2026-08-21 against a real fr-par account (#390).", + "issue": "https://github.com/stephrobert/feint/issues/391" }, { - "file": "scaleway/scw-billed-shapes.jsonl", - "operation": "lb/v1/ZonedAPI.GetLB", + "file": "scaleway/scw-refusals.jsonl", + "operation": "instance/v1/API.CreatePlacementGroup", "kind": "absent", - "path": "private_network_count", - "reason": "THIS EMULATOR'S LIFECYCLE TRANSITIONS ARE IMMEDIATE, and this is that decision seen from lb/v1. A DeleteLB removes the balancer at once here; upstream the read that FOLLOWS the delete still answers 200 with status `to_delete`, and only the read after that answers 404. corpus/scaleway/scw-billed-shapes.jsonl seq 47-50 is the delete, one 200, then two 404s. So one read of the recording meets a 404 here where the cloud answered a whole balancer, and a 404 reports every field of that body as absent -- nineteen findings for one instant. Reproducing it means giving a delete a terminal state that a later read clears, which is a change to the lifecycle of every kind and to what `nothing left behind` means for every conformance suite -- the same argument #380 records for Outscale, one cloud out. It goes the day a delete is asynchronous here. Recorded 2026-08-24 (#427).", - "issue": "https://github.com/stephrobert/feint/issues/434" + "path": "message", + "reason": "The create names a project identifier that does not exist and the cloud refuses it before allocating anything; this emulator accepts any project as an isolation boundary (projectOf, scopeOf) and creates the resource. Reproducing the refusal means the emulator must declare which project exists, which removes the multi-project behaviour it was built with, so it is a product decision rather than a patch. Recorded 2026-08-21 against a real fr-par account (#390).", + "issue": "https://github.com/stephrobert/feint/issues/391" }, { - "file": "scaleway/scw-billed-shapes.jsonl", - "operation": "lb/v1/ZonedAPI.GetLB", + "file": "scaleway/scw-refusals.jsonl", + "operation": "instance/v1/API.CreatePlacementGroup", "kind": "absent", - "path": "project_id", - "reason": "THIS EMULATOR'S LIFECYCLE TRANSITIONS ARE IMMEDIATE, and this is that decision seen from lb/v1. A DeleteLB removes the balancer at once here; upstream the read that FOLLOWS the delete still answers 200 with status `to_delete`, and only the read after that answers 404. corpus/scaleway/scw-billed-shapes.jsonl seq 47-50 is the delete, one 200, then two 404s. So one read of the recording meets a 404 here where the cloud answered a whole balancer, and a 404 reports every field of that body as absent -- nineteen findings for one instant. Reproducing it means giving a delete a terminal state that a later read clears, which is a change to the lifecycle of every kind and to what `nothing left behind` means for every conformance suite -- the same argument #380 records for Outscale, one cloud out. It goes the day a delete is asynchronous here. Recorded 2026-08-24 (#427).", - "issue": "https://github.com/stephrobert/feint/issues/434" + "path": "type", + "reason": "The create names a project identifier that does not exist and the cloud refuses it before allocating anything; this emulator accepts any project as an isolation boundary (projectOf, scopeOf) and creates the resource. Reproducing the refusal means the emulator must declare which project exists, which removes the multi-project behaviour it was built with, so it is a product decision rather than a patch. Recorded 2026-08-21 against a real fr-par account (#390).", + "issue": "https://github.com/stephrobert/feint/issues/391" }, { - "file": "scaleway/scw-billed-shapes.jsonl", - "operation": "lb/v1/ZonedAPI.GetLB", - "kind": "absent", - "path": "region", - "reason": "THIS EMULATOR'S LIFECYCLE TRANSITIONS ARE IMMEDIATE, and this is that decision seen from lb/v1. A DeleteLB removes the balancer at once here; upstream the read that FOLLOWS the delete still answers 200 with status `to_delete`, and only the read after that answers 404. corpus/scaleway/scw-billed-shapes.jsonl seq 47-50 is the delete, one 200, then two 404s. So one read of the recording meets a 404 here where the cloud answered a whole balancer, and a 404 reports every field of that body as absent -- nineteen findings for one instant. Reproducing it means giving a delete a terminal state that a later read clears, which is a change to the lifecycle of every kind and to what `nothing left behind` means for every conformance suite -- the same argument #380 records for Outscale, one cloud out. It goes the day a delete is asynchronous here. Recorded 2026-08-24 (#427).", - "issue": "https://github.com/stephrobert/feint/issues/434" + "file": "scaleway/scw-refusals.jsonl", + "operation": "instance/v1/API.CreatePlacementGroup", + "kind": "status", + "path": "", + "reason": "The create names a project identifier that does not exist and the cloud refuses it before allocating anything; this emulator accepts any project as an isolation boundary (projectOf, scopeOf) and creates the resource. Reproducing the refusal means the emulator must declare which project exists, which removes the multi-project behaviour it was built with, so it is a product decision rather than a patch. Recorded 2026-08-21 against a real fr-par account (#390).", + "issue": "https://github.com/stephrobert/feint/issues/391" }, { - "file": "scaleway/scw-billed-shapes.jsonl", - "operation": "lb/v1/ZonedAPI.GetLB", + "file": "scaleway/scw-refusals.jsonl", + "operation": "instance/v1/API.CreateSecurityGroup", "kind": "absent", - "path": "route_count", - "reason": "THIS EMULATOR'S LIFECYCLE TRANSITIONS ARE IMMEDIATE, and this is that decision seen from lb/v1. A DeleteLB removes the balancer at once here; upstream the read that FOLLOWS the delete still answers 200 with status `to_delete`, and only the read after that answers 404. corpus/scaleway/scw-billed-shapes.jsonl seq 47-50 is the delete, one 200, then two 404s. So one read of the recording meets a 404 here where the cloud answered a whole balancer, and a 404 reports every field of that body as absent -- nineteen findings for one instant. Reproducing it means giving a delete a terminal state that a later read clears, which is a change to the lifecycle of every kind and to what `nothing left behind` means for every conformance suite -- the same argument #380 records for Outscale, one cloud out. It goes the day a delete is asynchronous here. Recorded 2026-08-24 (#427).", - "issue": "https://github.com/stephrobert/feint/issues/434" + "path": "details", + "reason": "The create names a project identifier that does not exist and the cloud refuses it before allocating anything; this emulator accepts any project as an isolation boundary (projectOf, scopeOf) and creates the resource. Reproducing the refusal means the emulator must declare which project exists, which removes the multi-project behaviour it was built with, so it is a product decision rather than a patch. Recorded 2026-08-21 against a real fr-par account (#390).", + "issue": "https://github.com/stephrobert/feint/issues/391" }, { - "file": "scaleway/scw-billed-shapes.jsonl", - "operation": "lb/v1/ZonedAPI.GetLB", + "file": "scaleway/scw-refusals.jsonl", + "operation": "instance/v1/API.CreateSecurityGroup", "kind": "absent", - "path": "ssl_compatibility_level", - "reason": "THIS EMULATOR'S LIFECYCLE TRANSITIONS ARE IMMEDIATE, and this is that decision seen from lb/v1. A DeleteLB removes the balancer at once here; upstream the read that FOLLOWS the delete still answers 200 with status `to_delete`, and only the read after that answers 404. corpus/scaleway/scw-billed-shapes.jsonl seq 47-50 is the delete, one 200, then two 404s. So one read of the recording meets a 404 here where the cloud answered a whole balancer, and a 404 reports every field of that body as absent -- nineteen findings for one instant. Reproducing it means giving a delete a terminal state that a later read clears, which is a change to the lifecycle of every kind and to what `nothing left behind` means for every conformance suite -- the same argument #380 records for Outscale, one cloud out. It goes the day a delete is asynchronous here. Recorded 2026-08-24 (#427).", - "issue": "https://github.com/stephrobert/feint/issues/434" + "path": "message", + "reason": "The create names a project identifier that does not exist and the cloud refuses it before allocating anything; this emulator accepts any project as an isolation boundary (projectOf, scopeOf) and creates the resource. Reproducing the refusal means the emulator must declare which project exists, which removes the multi-project behaviour it was built with, so it is a product decision rather than a patch. Recorded 2026-08-21 against a real fr-par account (#390).", + "issue": "https://github.com/stephrobert/feint/issues/391" }, { - "file": "scaleway/scw-billed-shapes.jsonl", - "operation": "lb/v1/ZonedAPI.GetLB", + "file": "scaleway/scw-refusals.jsonl", + "operation": "instance/v1/API.CreateSecurityGroup", "kind": "absent", - "path": "status", - "reason": "THIS EMULATOR'S LIFECYCLE TRANSITIONS ARE IMMEDIATE, and this is that decision seen from lb/v1. A DeleteLB removes the balancer at once here; upstream the read that FOLLOWS the delete still answers 200 with status `to_delete`, and only the read after that answers 404. corpus/scaleway/scw-billed-shapes.jsonl seq 47-50 is the delete, one 200, then two 404s. So one read of the recording meets a 404 here where the cloud answered a whole balancer, and a 404 reports every field of that body as absent -- nineteen findings for one instant. Reproducing it means giving a delete a terminal state that a later read clears, which is a change to the lifecycle of every kind and to what `nothing left behind` means for every conformance suite -- the same argument #380 records for Outscale, one cloud out. It goes the day a delete is asynchronous here. Recorded 2026-08-24 (#427).", - "issue": "https://github.com/stephrobert/feint/issues/434" + "path": "type", + "reason": "The create names a project identifier that does not exist and the cloud refuses it before allocating anything; this emulator accepts any project as an isolation boundary (projectOf, scopeOf) and creates the resource. Reproducing the refusal means the emulator must declare which project exists, which removes the multi-project behaviour it was built with, so it is a product decision rather than a patch. Recorded 2026-08-21 against a real fr-par account (#390).", + "issue": "https://github.com/stephrobert/feint/issues/391" }, { - "file": "scaleway/scw-billed-shapes.jsonl", - "operation": "lb/v1/ZonedAPI.GetLB", - "kind": "absent", - "path": "subscriber", - "reason": "THIS EMULATOR'S LIFECYCLE TRANSITIONS ARE IMMEDIATE, and this is that decision seen from lb/v1. A DeleteLB removes the balancer at once here; upstream the read that FOLLOWS the delete still answers 200 with status `to_delete`, and only the read after that answers 404. corpus/scaleway/scw-billed-shapes.jsonl seq 47-50 is the delete, one 200, then two 404s. So one read of the recording meets a 404 here where the cloud answered a whole balancer, and a 404 reports every field of that body as absent -- nineteen findings for one instant. Reproducing it means giving a delete a terminal state that a later read clears, which is a change to the lifecycle of every kind and to what `nothing left behind` means for every conformance suite -- the same argument #380 records for Outscale, one cloud out. It goes the day a delete is asynchronous here. Recorded 2026-08-24 (#427).", - "issue": "https://github.com/stephrobert/feint/issues/434" + "file": "scaleway/scw-refusals.jsonl", + "operation": "instance/v1/API.CreateSecurityGroup", + "kind": "status", + "path": "", + "reason": "The create names a project identifier that does not exist and the cloud refuses it before allocating anything; this emulator accepts any project as an isolation boundary (projectOf, scopeOf) and creates the resource. Reproducing the refusal means the emulator must declare which project exists, which removes the multi-project behaviour it was built with, so it is a product decision rather than a patch. Recorded 2026-08-21 against a real fr-par account (#390).", + "issue": "https://github.com/stephrobert/feint/issues/391" }, { - "file": "scaleway/scw-billed-shapes.jsonl", - "operation": "lb/v1/ZonedAPI.GetLB", + "file": "scaleway/scw-refusals.jsonl", + "operation": "instance/v1/API.CreateServer", "kind": "absent", - "path": "tags", - "reason": "THIS EMULATOR'S LIFECYCLE TRANSITIONS ARE IMMEDIATE, and this is that decision seen from lb/v1. A DeleteLB removes the balancer at once here; upstream the read that FOLLOWS the delete still answers 200 with status `to_delete`, and only the read after that answers 404. corpus/scaleway/scw-billed-shapes.jsonl seq 47-50 is the delete, one 200, then two 404s. So one read of the recording meets a 404 here where the cloud answered a whole balancer, and a 404 reports every field of that body as absent -- nineteen findings for one instant. Reproducing it means giving a delete a terminal state that a later read clears, which is a change to the lifecycle of every kind and to what `nothing left behind` means for every conformance suite -- the same argument #380 records for Outscale, one cloud out. It goes the day a delete is asynchronous here. Recorded 2026-08-24 (#427).", - "issue": "https://github.com/stephrobert/feint/issues/434" + "path": "details", + "reason": "The create names a project identifier that does not exist and the cloud refuses it before allocating anything; this emulator accepts any project as an isolation boundary (projectOf, scopeOf) and creates the resource. Reproducing the refusal means the emulator must declare which project exists, which removes the multi-project behaviour it was built with, so it is a product decision rather than a patch. Recorded 2026-08-21 against a real fr-par account (#390).", + "issue": "https://github.com/stephrobert/feint/issues/391" }, { - "file": "scaleway/scw-billed-shapes.jsonl", - "operation": "lb/v1/ZonedAPI.GetLB", + "file": "scaleway/scw-refusals.jsonl", + "operation": "instance/v1/API.CreateServer", "kind": "absent", - "path": "updated_at", - "reason": "THIS EMULATOR'S LIFECYCLE TRANSITIONS ARE IMMEDIATE, and this is that decision seen from lb/v1. A DeleteLB removes the balancer at once here; upstream the read that FOLLOWS the delete still answers 200 with status `to_delete`, and only the read after that answers 404. corpus/scaleway/scw-billed-shapes.jsonl seq 47-50 is the delete, one 200, then two 404s. So one read of the recording meets a 404 here where the cloud answered a whole balancer, and a 404 reports every field of that body as absent -- nineteen findings for one instant. Reproducing it means giving a delete a terminal state that a later read clears, which is a change to the lifecycle of every kind and to what `nothing left behind` means for every conformance suite -- the same argument #380 records for Outscale, one cloud out. It goes the day a delete is asynchronous here. Recorded 2026-08-24 (#427).", - "issue": "https://github.com/stephrobert/feint/issues/434" + "path": "message", + "reason": "The create names a project identifier that does not exist and the cloud refuses it before allocating anything; this emulator accepts any project as an isolation boundary (projectOf, scopeOf) and creates the resource. Reproducing the refusal means the emulator must declare which project exists, which removes the multi-project behaviour it was built with, so it is a product decision rather than a patch. Recorded 2026-08-21 against a real fr-par account (#390).", + "issue": "https://github.com/stephrobert/feint/issues/391" }, { - "file": "scaleway/scw-billed-shapes.jsonl", - "operation": "lb/v1/ZonedAPI.GetLB", + "file": "scaleway/scw-refusals.jsonl", + "operation": "instance/v1/API.CreateServer", "kind": "absent", - "path": "zone", - "reason": "THIS EMULATOR'S LIFECYCLE TRANSITIONS ARE IMMEDIATE, and this is that decision seen from lb/v1. A DeleteLB removes the balancer at once here; upstream the read that FOLLOWS the delete still answers 200 with status `to_delete`, and only the read after that answers 404. corpus/scaleway/scw-billed-shapes.jsonl seq 47-50 is the delete, one 200, then two 404s. So one read of the recording meets a 404 here where the cloud answered a whole balancer, and a 404 reports every field of that body as absent -- nineteen findings for one instant. Reproducing it means giving a delete a terminal state that a later read clears, which is a change to the lifecycle of every kind and to what `nothing left behind` means for every conformance suite -- the same argument #380 records for Outscale, one cloud out. It goes the day a delete is asynchronous here. Recorded 2026-08-24 (#427).", - "issue": "https://github.com/stephrobert/feint/issues/434" + "path": "type", + "reason": "The create names a project identifier that does not exist and the cloud refuses it before allocating anything; this emulator accepts any project as an isolation boundary (projectOf, scopeOf) and creates the resource. Reproducing the refusal means the emulator must declare which project exists, which removes the multi-project behaviour it was built with, so it is a product decision rather than a patch. Recorded 2026-08-21 against a real fr-par account (#390).", + "issue": "https://github.com/stephrobert/feint/issues/391" }, { - "file": "scaleway/scw-billed-shapes.jsonl", - "operation": "lb/v1/ZonedAPI.GetLB", + "file": "scaleway/scw-refusals.jsonl", + "operation": "instance/v1/API.CreateServer", "kind": "status", "path": "", - "reason": "THIS EMULATOR'S LIFECYCLE TRANSITIONS ARE IMMEDIATE, and this is that decision seen from lb/v1. A DeleteLB removes the balancer at once here; upstream the read that FOLLOWS the delete still answers 200 with status `to_delete`, and only the read after that answers 404. corpus/scaleway/scw-billed-shapes.jsonl seq 47-50 is the delete, one 200, then two 404s. So one read of the recording meets a 404 here where the cloud answered a whole balancer, and a 404 reports every field of that body as absent -- nineteen findings for one instant. Reproducing it means giving a delete a terminal state that a later read clears, which is a change to the lifecycle of every kind and to what `nothing left behind` means for every conformance suite -- the same argument #380 records for Outscale, one cloud out. It goes the day a delete is asynchronous here. Recorded 2026-08-24 (#427).", - "issue": "https://github.com/stephrobert/feint/issues/434" + "reason": "The create names a project identifier that does not exist and the cloud refuses it before allocating anything; this emulator accepts any project as an isolation boundary (projectOf, scopeOf) and creates the resource. Reproducing the refusal means the emulator must declare which project exists, which removes the multi-project behaviour it was built with, so it is a product decision rather than a patch. Recorded 2026-08-21 against a real fr-par account (#390).", + "issue": "https://github.com/stephrobert/feint/issues/391" }, { - "file": "scaleway/scw-billed-shapes.jsonl", - "operation": "vpcgw/v2/API.GetGateway", + "file": "scaleway/scw-refusals.jsonl", + "operation": "instance/v1/API.CreateVolume", "kind": "absent", - "path": "message", - "reason": "THIS IS THE RECORDING, NOT THE PACK, and the recording proves it in its own sequence numbers. The gateway goes from 200 on its seventh read to 404 on its eighth WITH NO DELETE ANYWHERE IN THE FILE, and its address follows: the recorded DeleteIP answers 404 and the recorded GetIP answers 404 too. The destruction did not travel through the proxy. STATED RATHER THAN ASSUMED: corpus/scaleway/scw-billed-shapes.jsonl holds three recording sessions whose own sequence numbers run 1..14, 1..43 and 1..65 with no gap, so nothing was dropped between the request that read the gateway and the request that could not find it, and no DELETE on /vpc-gw/v2/.../gateways/ exists in that file at all. This emulator was never asked to delete the gateway, so it still answers it -- and its DeleteIP refuses with 400 because the address is still attached to a gateway nobody destroyed. Answering 404 for an object nobody destroyed would be the lie this project exists to avoid. The `absent` findings on these operations are the fields of the cloud's 404 body, which is the same cause seen from the body. It goes when the gateway is recorded again with its destruction in the transcript. Recorded 2026-08-24 (#427).", - "issue": "https://github.com/stephrobert/feint/issues/435" + "path": "details", + "reason": "The cloud has retired b_ssd and refuses it at creation with a constraint on volume_type; this emulator accepts it and, worse, mints it for every root volume (rootVolume, servers.go). It is drift the surface scan cannot see, because CreateVolume's Go signature is unchanged.", + "issue": "https://github.com/stephrobert/feint/issues/393" }, { - "file": "scaleway/scw-billed-shapes.jsonl", - "operation": "vpcgw/v2/API.GetGateway", + "file": "scaleway/scw-refusals.jsonl", + "operation": "instance/v1/API.CreateVolume", "kind": "absent", - "path": "resource", - "reason": "THIS IS THE RECORDING, NOT THE PACK, and the recording proves it in its own sequence numbers. The gateway goes from 200 on its seventh read to 404 on its eighth WITH NO DELETE ANYWHERE IN THE FILE, and its address follows: the recorded DeleteIP answers 404 and the recorded GetIP answers 404 too. The destruction did not travel through the proxy. STATED RATHER THAN ASSUMED: corpus/scaleway/scw-billed-shapes.jsonl holds three recording sessions whose own sequence numbers run 1..14, 1..43 and 1..65 with no gap, so nothing was dropped between the request that read the gateway and the request that could not find it, and no DELETE on /vpc-gw/v2/.../gateways/ exists in that file at all. This emulator was never asked to delete the gateway, so it still answers it -- and its DeleteIP refuses with 400 because the address is still attached to a gateway nobody destroyed. Answering 404 for an object nobody destroyed would be the lie this project exists to avoid. The `absent` findings on these operations are the fields of the cloud's 404 body, which is the same cause seen from the body. It goes when the gateway is recorded again with its destruction in the transcript. Recorded 2026-08-24 (#427).", - "issue": "https://github.com/stephrobert/feint/issues/435" + "path": "message", + "reason": "The cloud has retired b_ssd and refuses it at creation with a constraint on volume_type; this emulator accepts it and, worse, mints it for every root volume (rootVolume, servers.go). It is drift the surface scan cannot see, because CreateVolume's Go signature is unchanged.", + "issue": "https://github.com/stephrobert/feint/issues/393" }, { - "file": "scaleway/scw-billed-shapes.jsonl", - "operation": "vpcgw/v2/API.GetGateway", + "file": "scaleway/scw-refusals.jsonl", + "operation": "instance/v1/API.CreateVolume", "kind": "absent", - "path": "resource_id", - "reason": "THIS IS THE RECORDING, NOT THE PACK, and the recording proves it in its own sequence numbers. The gateway goes from 200 on its seventh read to 404 on its eighth WITH NO DELETE ANYWHERE IN THE FILE, and its address follows: the recorded DeleteIP answers 404 and the recorded GetIP answers 404 too. The destruction did not travel through the proxy. STATED RATHER THAN ASSUMED: corpus/scaleway/scw-billed-shapes.jsonl holds three recording sessions whose own sequence numbers run 1..14, 1..43 and 1..65 with no gap, so nothing was dropped between the request that read the gateway and the request that could not find it, and no DELETE on /vpc-gw/v2/.../gateways/ exists in that file at all. This emulator was never asked to delete the gateway, so it still answers it -- and its DeleteIP refuses with 400 because the address is still attached to a gateway nobody destroyed. Answering 404 for an object nobody destroyed would be the lie this project exists to avoid. The `absent` findings on these operations are the fields of the cloud's 404 body, which is the same cause seen from the body. It goes when the gateway is recorded again with its destruction in the transcript. Recorded 2026-08-24 (#427).", - "issue": "https://github.com/stephrobert/feint/issues/435" + "path": "type", + "reason": "The cloud has retired b_ssd and refuses it at creation with a constraint on volume_type; this emulator accepts it and, worse, mints it for every root volume (rootVolume, servers.go). It is drift the surface scan cannot see, because CreateVolume's Go signature is unchanged.", + "issue": "https://github.com/stephrobert/feint/issues/393" }, { - "file": "scaleway/scw-billed-shapes.jsonl", - "operation": "vpcgw/v2/API.UpdateGateway", + "file": "scaleway/scw-refusals.jsonl", + "operation": "instance/v1/API.CreateVolume", "kind": "status", "path": "", - "reason": "THIS EMULATOR'S LIFECYCLE TRANSITIONS ARE IMMEDIATE, and this is that decision seen from vpcgw/v2. A gateway is `running` the instant it is created here; upstream it is `allocating` for a few seconds, and both an UpdateGateway and a CreateGatewayNetwork issued in that window are refused with 409 and a body naming the state (current_state: allocating, plus message, resource, resource_id and type). corpus/scaleway/scw-billed-shapes.jsonl seq 40 and 41 are those two refusals, and seq 39 and 51-56 show the gateway moving from allocating to running around them. Both succeed here, so the status differs and the error body's own fields read as absent -- the `absent` findings on these two operations are the same cause seen from the body, not fields the pack fails to serve. Serving the refusal would mean inventing the window, and a guard for a state nothing can enter is a control that can never fire. It goes the day a gateway allocates. Recorded 2026-08-24 (#427).", - "issue": "https://github.com/stephrobert/feint/issues/435" - }, - { - "file": "scaleway/scw-billed-shapes.jsonl", - "operation": "vpcgw/v2/API.UpdateGateway", - "kind": "absent", - "path": "current_state", - "reason": "THIS EMULATOR'S LIFECYCLE TRANSITIONS ARE IMMEDIATE, and this is that decision seen from vpcgw/v2. A gateway is `running` the instant it is created here; upstream it is `allocating` for a few seconds, and both an UpdateGateway and a CreateGatewayNetwork issued in that window are refused with 409 and a body naming the state (current_state: allocating, plus message, resource, resource_id and type). corpus/scaleway/scw-billed-shapes.jsonl seq 40 and 41 are those two refusals, and seq 39 and 51-56 show the gateway moving from allocating to running around them. Both succeed here, so the status differs and the error body's own fields read as absent -- the `absent` findings on these two operations are the same cause seen from the body, not fields the pack fails to serve. Serving the refusal would mean inventing the window, and a guard for a state nothing can enter is a control that can never fire. It goes the day a gateway allocates. Recorded 2026-08-24 (#427).", - "issue": "https://github.com/stephrobert/feint/issues/435" + "reason": "The cloud has retired b_ssd and refuses it at creation with a constraint on volume_type; this emulator accepts it and, worse, mints it for every root volume (rootVolume, servers.go). It is drift the surface scan cannot see, because CreateVolume's Go signature is unchanged.", + "issue": "https://github.com/stephrobert/feint/issues/393" }, { - "file": "scaleway/scw-billed-shapes.jsonl", - "operation": "vpcgw/v2/API.UpdateGateway", + "file": "scaleway/scw-refusals.jsonl", + "operation": "instance/v1/API.GetImage", "kind": "absent", "path": "message", - "reason": "THIS EMULATOR'S LIFECYCLE TRANSITIONS ARE IMMEDIATE, and this is that decision seen from vpcgw/v2. A gateway is `running` the instant it is created here; upstream it is `allocating` for a few seconds, and both an UpdateGateway and a CreateGatewayNetwork issued in that window are refused with 409 and a body naming the state (current_state: allocating, plus message, resource, resource_id and type). corpus/scaleway/scw-billed-shapes.jsonl seq 40 and 41 are those two refusals, and seq 39 and 51-56 show the gateway moving from allocating to running around them. Both succeed here, so the status differs and the error body's own fields read as absent -- the `absent` findings on these two operations are the same cause seen from the body, not fields the pack fails to serve. Serving the refusal would mean inventing the window, and a guard for a state nothing can enter is a control that can never fire. It goes the day a gateway allocates. Recorded 2026-08-24 (#427).", - "issue": "https://github.com/stephrobert/feint/issues/435" + "reason": "An image identifier that names nothing answers success here by decision: getImage serves the default catalogue entry for an unknown ID (docs/limits.md, so a script holding a real one keeps working), the marketplace local-image list does the same, and the Outscale createVms never looks the image up. The cloud refuses all three. A fix has to say what an emulator with no account does with an image identifier it never minted, and corpus/outscale/oapi-cli-lifecycle.jsonl is what would grade the answer.", + "issue": "https://github.com/stephrobert/feint/issues/392" }, { - "file": "scaleway/scw-billed-shapes.jsonl", - "operation": "vpcgw/v2/API.UpdateGateway", + "file": "scaleway/scw-refusals.jsonl", + "operation": "instance/v1/API.GetImage", "kind": "absent", "path": "resource", - "reason": "THIS EMULATOR'S LIFECYCLE TRANSITIONS ARE IMMEDIATE, and this is that decision seen from vpcgw/v2. A gateway is `running` the instant it is created here; upstream it is `allocating` for a few seconds, and both an UpdateGateway and a CreateGatewayNetwork issued in that window are refused with 409 and a body naming the state (current_state: allocating, plus message, resource, resource_id and type). corpus/scaleway/scw-billed-shapes.jsonl seq 40 and 41 are those two refusals, and seq 39 and 51-56 show the gateway moving from allocating to running around them. Both succeed here, so the status differs and the error body's own fields read as absent -- the `absent` findings on these two operations are the same cause seen from the body, not fields the pack fails to serve. Serving the refusal would mean inventing the window, and a guard for a state nothing can enter is a control that can never fire. It goes the day a gateway allocates. Recorded 2026-08-24 (#427).", - "issue": "https://github.com/stephrobert/feint/issues/435" + "reason": "An image identifier that names nothing answers success here by decision: getImage serves the default catalogue entry for an unknown ID (docs/limits.md, so a script holding a real one keeps working), the marketplace local-image list does the same, and the Outscale createVms never looks the image up. The cloud refuses all three. A fix has to say what an emulator with no account does with an image identifier it never minted, and corpus/outscale/oapi-cli-lifecycle.jsonl is what would grade the answer.", + "issue": "https://github.com/stephrobert/feint/issues/392" }, { - "file": "scaleway/scw-billed-shapes.jsonl", - "operation": "vpcgw/v2/API.UpdateGateway", + "file": "scaleway/scw-refusals.jsonl", + "operation": "instance/v1/API.GetImage", "kind": "absent", "path": "resource_id", - "reason": "THIS EMULATOR'S LIFECYCLE TRANSITIONS ARE IMMEDIATE, and this is that decision seen from vpcgw/v2. A gateway is `running` the instant it is created here; upstream it is `allocating` for a few seconds, and both an UpdateGateway and a CreateGatewayNetwork issued in that window are refused with 409 and a body naming the state (current_state: allocating, plus message, resource, resource_id and type). corpus/scaleway/scw-billed-shapes.jsonl seq 40 and 41 are those two refusals, and seq 39 and 51-56 show the gateway moving from allocating to running around them. Both succeed here, so the status differs and the error body's own fields read as absent -- the `absent` findings on these two operations are the same cause seen from the body, not fields the pack fails to serve. Serving the refusal would mean inventing the window, and a guard for a state nothing can enter is a control that can never fire. It goes the day a gateway allocates. Recorded 2026-08-24 (#427).", - "issue": "https://github.com/stephrobert/feint/issues/435" - }, - { - "file": "scaleway/scw-billed-shapes.jsonl", - "operation": "vpcgw/v2/API.CreateGatewayNetwork", - "kind": "status", - "path": "", - "reason": "THIS EMULATOR'S LIFECYCLE TRANSITIONS ARE IMMEDIATE, and this is that decision seen from vpcgw/v2. A gateway is `running` the instant it is created here; upstream it is `allocating` for a few seconds, and both an UpdateGateway and a CreateGatewayNetwork issued in that window are refused with 409 and a body naming the state (current_state: allocating, plus message, resource, resource_id and type). corpus/scaleway/scw-billed-shapes.jsonl seq 40 and 41 are those two refusals, and seq 39 and 51-56 show the gateway moving from allocating to running around them. Both succeed here, so the status differs and the error body's own fields read as absent -- the `absent` findings on these two operations are the same cause seen from the body, not fields the pack fails to serve. Serving the refusal would mean inventing the window, and a guard for a state nothing can enter is a control that can never fire. It goes the day a gateway allocates. Recorded 2026-08-24 (#427).", - "issue": "https://github.com/stephrobert/feint/issues/435" + "reason": "An image identifier that names nothing answers success here by decision: getImage serves the default catalogue entry for an unknown ID (docs/limits.md, so a script holding a real one keeps working), the marketplace local-image list does the same, and the Outscale createVms never looks the image up. The cloud refuses all three. A fix has to say what an emulator with no account does with an image identifier it never minted, and corpus/outscale/oapi-cli-lifecycle.jsonl is what would grade the answer.", + "issue": "https://github.com/stephrobert/feint/issues/392" }, { - "file": "scaleway/scw-billed-shapes.jsonl", - "operation": "vpcgw/v2/API.CreateGatewayNetwork", + "file": "scaleway/scw-refusals.jsonl", + "operation": "instance/v1/API.GetImage", "kind": "absent", - "path": "current_state", - "reason": "THIS EMULATOR'S LIFECYCLE TRANSITIONS ARE IMMEDIATE, and this is that decision seen from vpcgw/v2. A gateway is `running` the instant it is created here; upstream it is `allocating` for a few seconds, and both an UpdateGateway and a CreateGatewayNetwork issued in that window are refused with 409 and a body naming the state (current_state: allocating, plus message, resource, resource_id and type). corpus/scaleway/scw-billed-shapes.jsonl seq 40 and 41 are those two refusals, and seq 39 and 51-56 show the gateway moving from allocating to running around them. Both succeed here, so the status differs and the error body's own fields read as absent -- the `absent` findings on these two operations are the same cause seen from the body, not fields the pack fails to serve. Serving the refusal would mean inventing the window, and a guard for a state nothing can enter is a control that can never fire. It goes the day a gateway allocates. Recorded 2026-08-24 (#427).", - "issue": "https://github.com/stephrobert/feint/issues/435" + "path": "type", + "reason": "An image identifier that names nothing answers success here by decision: getImage serves the default catalogue entry for an unknown ID (docs/limits.md, so a script holding a real one keeps working), the marketplace local-image list does the same, and the Outscale createVms never looks the image up. The cloud refuses all three. A fix has to say what an emulator with no account does with an image identifier it never minted, and corpus/outscale/oapi-cli-lifecycle.jsonl is what would grade the answer.", + "issue": "https://github.com/stephrobert/feint/issues/392" }, { - "file": "scaleway/scw-billed-shapes.jsonl", - "operation": "vpcgw/v2/API.CreateGatewayNetwork", - "kind": "absent", - "path": "message", - "reason": "THIS EMULATOR'S LIFECYCLE TRANSITIONS ARE IMMEDIATE, and this is that decision seen from vpcgw/v2. A gateway is `running` the instant it is created here; upstream it is `allocating` for a few seconds, and both an UpdateGateway and a CreateGatewayNetwork issued in that window are refused with 409 and a body naming the state (current_state: allocating, plus message, resource, resource_id and type). corpus/scaleway/scw-billed-shapes.jsonl seq 40 and 41 are those two refusals, and seq 39 and 51-56 show the gateway moving from allocating to running around them. Both succeed here, so the status differs and the error body's own fields read as absent -- the `absent` findings on these two operations are the same cause seen from the body, not fields the pack fails to serve. Serving the refusal would mean inventing the window, and a guard for a state nothing can enter is a control that can never fire. It goes the day a gateway allocates. Recorded 2026-08-24 (#427).", - "issue": "https://github.com/stephrobert/feint/issues/435" + "file": "scaleway/scw-refusals.jsonl", + "operation": "instance/v1/API.GetImage", + "kind": "status", + "path": "", + "reason": "An image identifier that names nothing answers success here by decision: getImage serves the default catalogue entry for an unknown ID (docs/limits.md, so a script holding a real one keeps working), the marketplace local-image list does the same, and the Outscale createVms never looks the image up. The cloud refuses all three. A fix has to say what an emulator with no account does with an image identifier it never minted, and corpus/outscale/oapi-cli-lifecycle.jsonl is what would grade the answer.", + "issue": "https://github.com/stephrobert/feint/issues/392" }, { - "file": "scaleway/scw-billed-shapes.jsonl", - "operation": "vpcgw/v2/API.CreateGatewayNetwork", + "file": "scaleway/scw-refusals.jsonl", + "operation": "lb/v1/ZonedAPI.CreateIP", "kind": "absent", - "path": "resource", - "reason": "THIS EMULATOR'S LIFECYCLE TRANSITIONS ARE IMMEDIATE, and this is that decision seen from vpcgw/v2. A gateway is `running` the instant it is created here; upstream it is `allocating` for a few seconds, and both an UpdateGateway and a CreateGatewayNetwork issued in that window are refused with 409 and a body naming the state (current_state: allocating, plus message, resource, resource_id and type). corpus/scaleway/scw-billed-shapes.jsonl seq 40 and 41 are those two refusals, and seq 39 and 51-56 show the gateway moving from allocating to running around them. Both succeed here, so the status differs and the error body's own fields read as absent -- the `absent` findings on these two operations are the same cause seen from the body, not fields the pack fails to serve. Serving the refusal would mean inventing the window, and a guard for a state nothing can enter is a control that can never fire. It goes the day a gateway allocates. Recorded 2026-08-24 (#427).", - "issue": "https://github.com/stephrobert/feint/issues/435" + "path": "details", + "reason": "The create names a project identifier that does not exist and the cloud refuses it before allocating anything; this emulator accepts any project as an isolation boundary (projectOf, scopeOf) and creates the resource. Reproducing the refusal means the emulator must declare which project exists, which removes the multi-project behaviour it was built with, so it is a product decision rather than a patch. Recorded 2026-08-21 against a real fr-par account (#390).", + "issue": "https://github.com/stephrobert/feint/issues/391" }, { - "file": "scaleway/scw-billed-shapes.jsonl", - "operation": "vpcgw/v2/API.CreateGatewayNetwork", + "file": "scaleway/scw-refusals.jsonl", + "operation": "lb/v1/ZonedAPI.CreateIP", "kind": "absent", - "path": "resource_id", - "reason": "THIS EMULATOR'S LIFECYCLE TRANSITIONS ARE IMMEDIATE, and this is that decision seen from vpcgw/v2. A gateway is `running` the instant it is created here; upstream it is `allocating` for a few seconds, and both an UpdateGateway and a CreateGatewayNetwork issued in that window are refused with 409 and a body naming the state (current_state: allocating, plus message, resource, resource_id and type). corpus/scaleway/scw-billed-shapes.jsonl seq 40 and 41 are those two refusals, and seq 39 and 51-56 show the gateway moving from allocating to running around them. Both succeed here, so the status differs and the error body's own fields read as absent -- the `absent` findings on these two operations are the same cause seen from the body, not fields the pack fails to serve. Serving the refusal would mean inventing the window, and a guard for a state nothing can enter is a control that can never fire. It goes the day a gateway allocates. Recorded 2026-08-24 (#427).", - "issue": "https://github.com/stephrobert/feint/issues/435" + "path": "message", + "reason": "The create names a project identifier that does not exist and the cloud refuses it before allocating anything; this emulator accepts any project as an isolation boundary (projectOf, scopeOf) and creates the resource. Reproducing the refusal means the emulator must declare which project exists, which removes the multi-project behaviour it was built with, so it is a product decision rather than a patch. Recorded 2026-08-21 against a real fr-par account (#390).", + "issue": "https://github.com/stephrobert/feint/issues/391" }, { - "file": "scaleway/scw-billed-shapes.jsonl", - "operation": "vpcgw/v2/API.CreateGatewayNetwork", + "file": "scaleway/scw-refusals.jsonl", + "operation": "lb/v1/ZonedAPI.CreateIP", "kind": "absent", "path": "type", - "reason": "THIS EMULATOR'S LIFECYCLE TRANSITIONS ARE IMMEDIATE, and this is that decision seen from vpcgw/v2. A gateway is `running` the instant it is created here; upstream it is `allocating` for a few seconds, and both an UpdateGateway and a CreateGatewayNetwork issued in that window are refused with 409 and a body naming the state (current_state: allocating, plus message, resource, resource_id and type). corpus/scaleway/scw-billed-shapes.jsonl seq 40 and 41 are those two refusals, and seq 39 and 51-56 show the gateway moving from allocating to running around them. Both succeed here, so the status differs and the error body's own fields read as absent -- the `absent` findings on these two operations are the same cause seen from the body, not fields the pack fails to serve. Serving the refusal would mean inventing the window, and a guard for a state nothing can enter is a control that can never fire. It goes the day a gateway allocates. Recorded 2026-08-24 (#427).", - "issue": "https://github.com/stephrobert/feint/issues/435" + "reason": "The create names a project identifier that does not exist and the cloud refuses it before allocating anything; this emulator accepts any project as an isolation boundary (projectOf, scopeOf) and creates the resource. Reproducing the refusal means the emulator must declare which project exists, which removes the multi-project behaviour it was built with, so it is a product decision rather than a patch. Recorded 2026-08-21 against a real fr-par account (#390).", + "issue": "https://github.com/stephrobert/feint/issues/391" }, { - "file": "scaleway/scw-billed-shapes.jsonl", - "operation": "vpcgw/v2/API.GetGateway", + "file": "scaleway/scw-refusals.jsonl", + "operation": "lb/v1/ZonedAPI.CreateIP", "kind": "status", "path": "", - "reason": "THIS IS THE RECORDING, NOT THE PACK, and the recording proves it in its own sequence numbers. The gateway goes from 200 on its seventh read to 404 on its eighth WITH NO DELETE ANYWHERE IN THE FILE, and its address follows: the recorded DeleteIP answers 404 and the recorded GetIP answers 404 too. The destruction did not travel through the proxy. STATED RATHER THAN ASSUMED: corpus/scaleway/scw-billed-shapes.jsonl holds three recording sessions whose own sequence numbers run 1..14, 1..43 and 1..65 with no gap, so nothing was dropped between the request that read the gateway and the request that could not find it, and no DELETE on /vpc-gw/v2/.../gateways/ exists in that file at all. This emulator was never asked to delete the gateway, so it still answers it -- and its DeleteIP refuses with 400 because the address is still attached to a gateway nobody destroyed. Answering 404 for an object nobody destroyed would be the lie this project exists to avoid. The `absent` findings on these operations are the fields of the cloud's 404 body, which is the same cause seen from the body. It goes when the gateway is recorded again with its destruction in the transcript. Recorded 2026-08-24 (#427).", - "issue": "https://github.com/stephrobert/feint/issues/435" + "reason": "The create names a project identifier that does not exist and the cloud refuses it before allocating anything; this emulator accepts any project as an isolation boundary (projectOf, scopeOf) and creates the resource. Reproducing the refusal means the emulator must declare which project exists, which removes the multi-project behaviour it was built with, so it is a product decision rather than a patch. Recorded 2026-08-21 against a real fr-par account (#390).", + "issue": "https://github.com/stephrobert/feint/issues/391" }, { - "file": "scaleway/scw-billed-shapes.jsonl", - "operation": "vpcgw/v2/API.DeleteIP", - "kind": "status", - "path": "", - "reason": "THIS IS THE RECORDING, NOT THE PACK, and the recording proves it in its own sequence numbers. The gateway goes from 200 on its seventh read to 404 on its eighth WITH NO DELETE ANYWHERE IN THE FILE, and its address follows: the recorded DeleteIP answers 404 and the recorded GetIP answers 404 too. The destruction did not travel through the proxy. STATED RATHER THAN ASSUMED: corpus/scaleway/scw-billed-shapes.jsonl holds three recording sessions whose own sequence numbers run 1..14, 1..43 and 1..65 with no gap, so nothing was dropped between the request that read the gateway and the request that could not find it, and no DELETE on /vpc-gw/v2/.../gateways/ exists in that file at all. This emulator was never asked to delete the gateway, so it still answers it -- and its DeleteIP refuses with 400 because the address is still attached to a gateway nobody destroyed. Answering 404 for an object nobody destroyed would be the lie this project exists to avoid. The `absent` findings on these operations are the fields of the cloud's 404 body, which is the same cause seen from the body. It goes when the gateway is recorded again with its destruction in the transcript. Recorded 2026-08-24 (#427).", - "issue": "https://github.com/stephrobert/feint/issues/435" + "file": "scaleway/scw-refusals.jsonl", + "operation": "lb/v1/ZonedAPI.CreateLB", + "kind": "absent", + "path": "details", + "reason": "The create names a project identifier that does not exist and the cloud refuses it before allocating anything; this emulator accepts any project as an isolation boundary (projectOf, scopeOf) and creates the resource. Reproducing the refusal means the emulator must declare which project exists, which removes the multi-project behaviour it was built with, so it is a product decision rather than a patch. Recorded 2026-08-21 against a real fr-par account (#390).", + "issue": "https://github.com/stephrobert/feint/issues/391" }, { - "file": "scaleway/scw-billed-shapes.jsonl", - "operation": "vpcgw/v2/API.DeleteIP", + "file": "scaleway/scw-refusals.jsonl", + "operation": "lb/v1/ZonedAPI.CreateLB", "kind": "absent", "path": "message", - "reason": "THIS IS THE RECORDING, NOT THE PACK, and the recording proves it in its own sequence numbers. The gateway goes from 200 on its seventh read to 404 on its eighth WITH NO DELETE ANYWHERE IN THE FILE, and its address follows: the recorded DeleteIP answers 404 and the recorded GetIP answers 404 too. The destruction did not travel through the proxy. STATED RATHER THAN ASSUMED: corpus/scaleway/scw-billed-shapes.jsonl holds three recording sessions whose own sequence numbers run 1..14, 1..43 and 1..65 with no gap, so nothing was dropped between the request that read the gateway and the request that could not find it, and no DELETE on /vpc-gw/v2/.../gateways/ exists in that file at all. This emulator was never asked to delete the gateway, so it still answers it -- and its DeleteIP refuses with 400 because the address is still attached to a gateway nobody destroyed. Answering 404 for an object nobody destroyed would be the lie this project exists to avoid. The `absent` findings on these operations are the fields of the cloud's 404 body, which is the same cause seen from the body. It goes when the gateway is recorded again with its destruction in the transcript. Recorded 2026-08-24 (#427).", - "issue": "https://github.com/stephrobert/feint/issues/435" + "reason": "The create names a project identifier that does not exist and the cloud refuses it before allocating anything; this emulator accepts any project as an isolation boundary (projectOf, scopeOf) and creates the resource. Reproducing the refusal means the emulator must declare which project exists, which removes the multi-project behaviour it was built with, so it is a product decision rather than a patch. Recorded 2026-08-21 against a real fr-par account (#390).", + "issue": "https://github.com/stephrobert/feint/issues/391" }, { - "file": "scaleway/scw-billed-shapes.jsonl", - "operation": "vpcgw/v2/API.GetIP", + "file": "scaleway/scw-refusals.jsonl", + "operation": "lb/v1/ZonedAPI.CreateLB", "kind": "status", "path": "", - "reason": "THIS IS THE RECORDING, NOT THE PACK, and the recording proves it in its own sequence numbers. The gateway goes from 200 on its seventh read to 404 on its eighth WITH NO DELETE ANYWHERE IN THE FILE, and its address follows: the recorded DeleteIP answers 404 and the recorded GetIP answers 404 too. The destruction did not travel through the proxy. STATED RATHER THAN ASSUMED: corpus/scaleway/scw-billed-shapes.jsonl holds three recording sessions whose own sequence numbers run 1..14, 1..43 and 1..65 with no gap, so nothing was dropped between the request that read the gateway and the request that could not find it, and no DELETE on /vpc-gw/v2/.../gateways/ exists in that file at all. This emulator was never asked to delete the gateway, so it still answers it -- and its DeleteIP refuses with 400 because the address is still attached to a gateway nobody destroyed. Answering 404 for an object nobody destroyed would be the lie this project exists to avoid. The `absent` findings on these operations are the fields of the cloud's 404 body, which is the same cause seen from the body. It goes when the gateway is recorded again with its destruction in the transcript. Recorded 2026-08-24 (#427).", - "issue": "https://github.com/stephrobert/feint/issues/435" + "reason": "The create names a project identifier that does not exist and the cloud refuses it before allocating anything; this emulator accepts any project as an isolation boundary (projectOf, scopeOf) and creates the resource. Reproducing the refusal means the emulator must declare which project exists, which removes the multi-project behaviour it was built with, so it is a product decision rather than a patch. Recorded 2026-08-21 against a real fr-par account (#390).", + "issue": "https://github.com/stephrobert/feint/issues/391" }, { - "file": "scaleway/scw-billed-shapes.jsonl", - "operation": "vpcgw/v2/API.GetIP", + "file": "scaleway/scw-refusals.jsonl", + "operation": "lb/v1/ZonedAPI.CreateRoute", + "kind": "status", + "path": "", + "reason": "Both sides refuse and disagree on which refusal it is: upstream validates the body before it resolves the parent (400 naming the field) where this pack resolves the parent first (404), and lb answers 403 where the frontend is simply absent. A client branches on the status, so the difference is not cosmetic; changing it is a rule about every handler rather than about these two.", + "issue": "https://github.com/stephrobert/feint/issues/394" + }, + { + "file": "scaleway/scw-refusals.jsonl", + "operation": "marketplace/v2/API.ListLocalImages", "kind": "absent", "path": "message", - "reason": "THIS IS THE RECORDING, NOT THE PACK, and the recording proves it in its own sequence numbers. The gateway goes from 200 on its seventh read to 404 on its eighth WITH NO DELETE ANYWHERE IN THE FILE, and its address follows: the recorded DeleteIP answers 404 and the recorded GetIP answers 404 too. The destruction did not travel through the proxy. STATED RATHER THAN ASSUMED: corpus/scaleway/scw-billed-shapes.jsonl holds three recording sessions whose own sequence numbers run 1..14, 1..43 and 1..65 with no gap, so nothing was dropped between the request that read the gateway and the request that could not find it, and no DELETE on /vpc-gw/v2/.../gateways/ exists in that file at all. This emulator was never asked to delete the gateway, so it still answers it -- and its DeleteIP refuses with 400 because the address is still attached to a gateway nobody destroyed. Answering 404 for an object nobody destroyed would be the lie this project exists to avoid. The `absent` findings on these operations are the fields of the cloud's 404 body, which is the same cause seen from the body. It goes when the gateway is recorded again with its destruction in the transcript. Recorded 2026-08-24 (#427).", - "issue": "https://github.com/stephrobert/feint/issues/435" + "reason": "An image identifier that names nothing answers success here by decision: getImage serves the default catalogue entry for an unknown ID (docs/limits.md, so a script holding a real one keeps working), the marketplace local-image list does the same, and the Outscale createVms never looks the image up. The cloud refuses all three. A fix has to say what an emulator with no account does with an image identifier it never minted, and corpus/outscale/oapi-cli-lifecycle.jsonl is what would grade the answer.", + "issue": "https://github.com/stephrobert/feint/issues/392" }, { - "file": "scaleway/scw-billed-shapes.jsonl", - "operation": "vpcgw/v2/API.GetIP", + "file": "scaleway/scw-refusals.jsonl", + "operation": "marketplace/v2/API.ListLocalImages", "kind": "absent", "path": "resource", - "reason": "THIS IS THE RECORDING, NOT THE PACK, and the recording proves it in its own sequence numbers. The gateway goes from 200 on its seventh read to 404 on its eighth WITH NO DELETE ANYWHERE IN THE FILE, and its address follows: the recorded DeleteIP answers 404 and the recorded GetIP answers 404 too. The destruction did not travel through the proxy. STATED RATHER THAN ASSUMED: corpus/scaleway/scw-billed-shapes.jsonl holds three recording sessions whose own sequence numbers run 1..14, 1..43 and 1..65 with no gap, so nothing was dropped between the request that read the gateway and the request that could not find it, and no DELETE on /vpc-gw/v2/.../gateways/ exists in that file at all. This emulator was never asked to delete the gateway, so it still answers it -- and its DeleteIP refuses with 400 because the address is still attached to a gateway nobody destroyed. Answering 404 for an object nobody destroyed would be the lie this project exists to avoid. The `absent` findings on these operations are the fields of the cloud's 404 body, which is the same cause seen from the body. It goes when the gateway is recorded again with its destruction in the transcript. Recorded 2026-08-24 (#427).", - "issue": "https://github.com/stephrobert/feint/issues/435" + "reason": "An image identifier that names nothing answers success here by decision: getImage serves the default catalogue entry for an unknown ID (docs/limits.md, so a script holding a real one keeps working), the marketplace local-image list does the same, and the Outscale createVms never looks the image up. The cloud refuses all three. A fix has to say what an emulator with no account does with an image identifier it never minted, and corpus/outscale/oapi-cli-lifecycle.jsonl is what would grade the answer.", + "issue": "https://github.com/stephrobert/feint/issues/392" }, { - "file": "scaleway/scw-billed-shapes.jsonl", - "operation": "vpcgw/v2/API.GetIP", + "file": "scaleway/scw-refusals.jsonl", + "operation": "marketplace/v2/API.ListLocalImages", "kind": "absent", "path": "resource_id", - "reason": "THIS IS THE RECORDING, NOT THE PACK, and the recording proves it in its own sequence numbers. The gateway goes from 200 on its seventh read to 404 on its eighth WITH NO DELETE ANYWHERE IN THE FILE, and its address follows: the recorded DeleteIP answers 404 and the recorded GetIP answers 404 too. The destruction did not travel through the proxy. STATED RATHER THAN ASSUMED: corpus/scaleway/scw-billed-shapes.jsonl holds three recording sessions whose own sequence numbers run 1..14, 1..43 and 1..65 with no gap, so nothing was dropped between the request that read the gateway and the request that could not find it, and no DELETE on /vpc-gw/v2/.../gateways/ exists in that file at all. This emulator was never asked to delete the gateway, so it still answers it -- and its DeleteIP refuses with 400 because the address is still attached to a gateway nobody destroyed. Answering 404 for an object nobody destroyed would be the lie this project exists to avoid. The `absent` findings on these operations are the fields of the cloud's 404 body, which is the same cause seen from the body. It goes when the gateway is recorded again with its destruction in the transcript. Recorded 2026-08-24 (#427).", - "issue": "https://github.com/stephrobert/feint/issues/435" + "reason": "An image identifier that names nothing answers success here by decision: getImage serves the default catalogue entry for an unknown ID (docs/limits.md, so a script holding a real one keeps working), the marketplace local-image list does the same, and the Outscale createVms never looks the image up. The cloud refuses all three. A fix has to say what an emulator with no account does with an image identifier it never minted, and corpus/outscale/oapi-cli-lifecycle.jsonl is what would grade the answer.", + "issue": "https://github.com/stephrobert/feint/issues/392" }, { - "file": "scaleway/scw-billed-shapes.jsonl", - "operation": "vpcgw/v2/API.GetIP", + "file": "scaleway/scw-refusals.jsonl", + "operation": "marketplace/v2/API.ListLocalImages", "kind": "absent", "path": "type", - "reason": "THIS IS THE RECORDING, NOT THE PACK, and the recording proves it in its own sequence numbers. The gateway goes from 200 on its seventh read to 404 on its eighth WITH NO DELETE ANYWHERE IN THE FILE, and its address follows: the recorded DeleteIP answers 404 and the recorded GetIP answers 404 too. The destruction did not travel through the proxy. STATED RATHER THAN ASSUMED: corpus/scaleway/scw-billed-shapes.jsonl holds three recording sessions whose own sequence numbers run 1..14, 1..43 and 1..65 with no gap, so nothing was dropped between the request that read the gateway and the request that could not find it, and no DELETE on /vpc-gw/v2/.../gateways/ exists in that file at all. This emulator was never asked to delete the gateway, so it still answers it -- and its DeleteIP refuses with 400 because the address is still attached to a gateway nobody destroyed. Answering 404 for an object nobody destroyed would be the lie this project exists to avoid. The `absent` findings on these operations are the fields of the cloud's 404 body, which is the same cause seen from the body. It goes when the gateway is recorded again with its destruction in the transcript. Recorded 2026-08-24 (#427).", - "issue": "https://github.com/stephrobert/feint/issues/435" + "reason": "An image identifier that names nothing answers success here by decision: getImage serves the default catalogue entry for an unknown ID (docs/limits.md, so a script holding a real one keeps working), the marketplace local-image list does the same, and the Outscale createVms never looks the image up. The cloud refuses all three. A fix has to say what an emulator with no account does with an image identifier it never minted, and corpus/outscale/oapi-cli-lifecycle.jsonl is what would grade the answer.", + "issue": "https://github.com/stephrobert/feint/issues/392" }, { - "file": "scaleway/scw-billed-shapes.jsonl", - "operation": "block/v1alpha1/API.DeleteSnapshot", - "kind": "type", + "file": "scaleway/scw-refusals.jsonl", + "operation": "marketplace/v2/API.ListLocalImages", + "kind": "status", "path": "", - "reason": "THIS EMULATOR'S LIFECYCLE TRANSITIONS ARE IMMEDIATE, which docs/limits.md states as a decision, and this is that decision seen from block/v1alpha1. A snapshot is `available` the instant it is cut here; upstream it is born in a transient state and a DeleteSnapshot issued while it settles is refused with 412 and a body. corpus/scaleway/scw-billed-shapes.jsonl seq 9-12 is exactly that sequence: the refused delete, a read that still finds the snapshot, the accepted delete, a read that does not. Here the FIRST delete succeeds, so the read between them finds nothing (404 against 200, which reports all twelve fields of the snapshot absent) and the second delete meets nothing (404 against 204). A refusal whose state is not reachable here cannot be served: a guard for a state nothing can enter is a control that can never fire, which is the rule docs/limits.md already draws for Outscale's InvalidVolumeState. It goes the day a snapshot has a settling state. Recorded 2026-08-24 (#427).", - "issue": "https://github.com/stephrobert/feint/issues/433" - }, - { - "file": "outscale/oapi-cli-lb-shapes.jsonl", - "operation": "osc/Client.ReadLoadBalancers", - "kind": "absent", - "path": "LoadBalancers[]", - "reason": "A field a real cloudgouv-eu-west-1 account answers and this emulator omits, recorded 2026-08-24 (#427). An image a client cuts from a machine carries its own device mapping and a FileLocation; a volume resize answers the TaskId that will finish it; a health check carries its Path. #437 carries the measured list and the reason each is a distinct decision.", - "issue": "https://github.com/stephrobert/feint/issues/437" - }, - { - "file": "outscale/oapi-cli-lb-shapes.jsonl", - "operation": "osc/Client.ReadLoadBalancers", - "kind": "absent", - "path": "LoadBalancers[].HealthCheck.Path", - "reason": "A field a real cloudgouv-eu-west-1 account answers and this emulator omits, recorded 2026-08-24 (#427). An image a client cuts from a machine carries its own device mapping and a FileLocation; a volume resize answers the TaskId that will finish it; a health check carries its Path. #437 carries the measured list and the reason each is a distinct decision.", - "issue": "https://github.com/stephrobert/feint/issues/437" + "reason": "An image identifier that names nothing answers success here by decision: getImage serves the default catalogue entry for an unknown ID (docs/limits.md, so a script holding a real one keeps working), the marketplace local-image list does the same, and the Outscale createVms never looks the image up. The cloud refuses all three. A fix has to say what an emulator with no account does with an image identifier it never minted, and corpus/outscale/oapi-cli-lifecycle.jsonl is what would grade the answer.", + "issue": "https://github.com/stephrobert/feint/issues/392" }, { - "file": "outscale/oapi-cli-lb-shapes.jsonl", - "operation": "osc/Client.ReadVms", + "file": "scaleway/scw-refusals.jsonl", + "operation": "vpc/v2/API.CreateRoute", "kind": "absent", - "path": "Vms[].BlockDeviceMappings[]", - "reason": "A machine's BlockDeviceMappings is where a client reads the root volume it must not delete. #378 served the key; this recording shows the ARRAY still answers empty where the cloud answers an element, because the emulated machine owns no root volume. Recorded 2026-08-24 (#427).", - "issue": "https://github.com/stephrobert/feint/issues/378" + "path": "details", + "reason": "Both sides refuse and disagree on which refusal it is: upstream validates the body before it resolves the parent (400 naming the field) where this pack resolves the parent first (404), and lb answers 403 where the frontend is simply absent. A client branches on the status, so the difference is not cosmetic; changing it is a rule about every handler rather than about these two.", + "issue": "https://github.com/stephrobert/feint/issues/394" }, { - "file": "outscale/oapi-cli-lb-shapes.jsonl", - "operation": "osc/Client.DeleteSecurityGroup", + "file": "scaleway/scw-refusals.jsonl", + "operation": "vpc/v2/API.CreateRoute", "kind": "status", "path": "", - "reason": "A delete is instantaneous here and asynchronous upstream, so the two disagree on every read of a teardown. MEASURED AGAIN on 2026-08-24 and at much greater length than #354 could (#427): this recording's teardown retries until a read proves the object gone, so it carries 26 DeleteLoadBalancer attempts and 24 DeleteNet ones, each answered by the cloud with a 400 or a 409 while the object settled and by this emulator with a 200 at the first ask. The 'absent' findings on the same operations are the same cause seen from the body: an Errors array where the cloud refused, or an empty list where the cloud still held the object. Nothing here is a field the pack fails to serve.", - "issue": "https://github.com/stephrobert/feint/issues/380" + "reason": "Both sides refuse and disagree on which refusal it is: upstream validates the body before it resolves the parent (400 naming the field) where this pack resolves the parent first (404), and lb answers 403 where the frontend is simply absent. A client branches on the status, so the difference is not cosmetic; changing it is a rule about every handler rather than about these two.", + "issue": "https://github.com/stephrobert/feint/issues/394" }, { - "file": "outscale/oapi-cli-lb-shapes.jsonl", - "operation": "osc/Client.DeleteSecurityGroup", + "file": "scaleway/scw-refusals.jsonl", + "operation": "vpc/v2/API.CreateVPC", "kind": "absent", - "path": "Errors", - "reason": "A delete is instantaneous here and asynchronous upstream, so the two disagree on every read of a teardown. MEASURED AGAIN on 2026-08-24 and at much greater length than #354 could (#427): this recording's teardown retries until a read proves the object gone, so it carries 26 DeleteLoadBalancer attempts and 24 DeleteNet ones, each answered by the cloud with a 400 or a 409 while the object settled and by this emulator with a 200 at the first ask. The 'absent' findings on the same operations are the same cause seen from the body: an Errors array where the cloud refused, or an empty list where the cloud still held the object. Nothing here is a field the pack fails to serve.", - "issue": "https://github.com/stephrobert/feint/issues/380" + "path": "message", + "reason": "The create names a project identifier that does not exist and the cloud refuses it before allocating anything; this emulator accepts any project as an isolation boundary (projectOf, scopeOf) and creates the resource. Reproducing the refusal means the emulator must declare which project exists, which removes the multi-project behaviour it was built with, so it is a product decision rather than a patch. Recorded 2026-08-21 against a real fr-par account (#390).", + "issue": "https://github.com/stephrobert/feint/issues/391" }, { - "file": "outscale/oapi-cli-lb-shapes.jsonl", - "operation": "osc/Client.ReadSecurityGroups", + "file": "scaleway/scw-refusals.jsonl", + "operation": "vpc/v2/API.CreateVPC", "kind": "absent", - "path": "SecurityGroups[]", - "reason": "A field a real cloudgouv-eu-west-1 account answers and this emulator omits, recorded 2026-08-24 (#427). An image a client cuts from a machine carries its own device mapping and a FileLocation; a volume resize answers the TaskId that will finish it; a health check carries its Path. #437 carries the measured list and the reason each is a distinct decision.", - "issue": "https://github.com/stephrobert/feint/issues/437" - }, - { - "file": "outscale/oapi-cli-lb-shapes.jsonl", - "operation": "osc/Client.DeleteSubnet", - "kind": "status", - "path": "", - "reason": "A delete is instantaneous here and asynchronous upstream, so the two disagree on every read of a teardown. MEASURED AGAIN on 2026-08-24 and at much greater length than #354 could (#427): this recording's teardown retries until a read proves the object gone, so it carries 26 DeleteLoadBalancer attempts and 24 DeleteNet ones, each answered by the cloud with a 400 or a 409 while the object settled and by this emulator with a 200 at the first ask. The 'absent' findings on the same operations are the same cause seen from the body: an Errors array where the cloud refused, or an empty list where the cloud still held the object. Nothing here is a field the pack fails to serve.", - "issue": "https://github.com/stephrobert/feint/issues/380" + "path": "resource", + "reason": "The create names a project identifier that does not exist and the cloud refuses it before allocating anything; this emulator accepts any project as an isolation boundary (projectOf, scopeOf) and creates the resource. Reproducing the refusal means the emulator must declare which project exists, which removes the multi-project behaviour it was built with, so it is a product decision rather than a patch. Recorded 2026-08-21 against a real fr-par account (#390).", + "issue": "https://github.com/stephrobert/feint/issues/391" }, { - "file": "outscale/oapi-cli-lb-shapes.jsonl", - "operation": "osc/Client.DeleteSubnet", + "file": "scaleway/scw-refusals.jsonl", + "operation": "vpc/v2/API.CreateVPC", "kind": "absent", - "path": "Errors", - "reason": "A delete is instantaneous here and asynchronous upstream, so the two disagree on every read of a teardown. MEASURED AGAIN on 2026-08-24 and at much greater length than #354 could (#427): this recording's teardown retries until a read proves the object gone, so it carries 26 DeleteLoadBalancer attempts and 24 DeleteNet ones, each answered by the cloud with a 400 or a 409 while the object settled and by this emulator with a 200 at the first ask. The 'absent' findings on the same operations are the same cause seen from the body: an Errors array where the cloud refused, or an empty list where the cloud still held the object. Nothing here is a field the pack fails to serve.", - "issue": "https://github.com/stephrobert/feint/issues/380" + "path": "resource_id", + "reason": "The create names a project identifier that does not exist and the cloud refuses it before allocating anything; this emulator accepts any project as an isolation boundary (projectOf, scopeOf) and creates the resource. Reproducing the refusal means the emulator must declare which project exists, which removes the multi-project behaviour it was built with, so it is a product decision rather than a patch. Recorded 2026-08-21 against a real fr-par account (#390).", + "issue": "https://github.com/stephrobert/feint/issues/391" }, { - "file": "outscale/oapi-cli-lb-shapes.jsonl", - "operation": "osc/Client.ReadSubnets", + "file": "scaleway/scw-refusals.jsonl", + "operation": "vpc/v2/API.CreateVPC", "kind": "absent", - "path": "Subnets[]", - "reason": "A field a real cloudgouv-eu-west-1 account answers and this emulator omits, recorded 2026-08-24 (#427). An image a client cuts from a machine carries its own device mapping and a FileLocation; a volume resize answers the TaskId that will finish it; a health check carries its Path. #437 carries the measured list and the reason each is a distinct decision.", - "issue": "https://github.com/stephrobert/feint/issues/437" + "path": "type", + "reason": "The create names a project identifier that does not exist and the cloud refuses it before allocating anything; this emulator accepts any project as an isolation boundary (projectOf, scopeOf) and creates the resource. Reproducing the refusal means the emulator must declare which project exists, which removes the multi-project behaviour it was built with, so it is a product decision rather than a patch. Recorded 2026-08-21 against a real fr-par account (#390).", + "issue": "https://github.com/stephrobert/feint/issues/391" }, { - "file": "outscale/oapi-cli-lb-shapes.jsonl", - "operation": "osc/Client.DeleteNet", + "file": "scaleway/scw-refusals.jsonl", + "operation": "vpc/v2/API.CreateVPC", "kind": "status", "path": "", - "reason": "A delete is instantaneous here and asynchronous upstream, so the two disagree on every read of a teardown. MEASURED AGAIN on 2026-08-24 and at much greater length than #354 could (#427): this recording's teardown retries until a read proves the object gone, so it carries 26 DeleteLoadBalancer attempts and 24 DeleteNet ones, each answered by the cloud with a 400 or a 409 while the object settled and by this emulator with a 200 at the first ask. The 'absent' findings on the same operations are the same cause seen from the body: an Errors array where the cloud refused, or an empty list where the cloud still held the object. Nothing here is a field the pack fails to serve.", - "issue": "https://github.com/stephrobert/feint/issues/380" + "reason": "The create names a project identifier that does not exist and the cloud refuses it before allocating anything; this emulator accepts any project as an isolation boundary (projectOf, scopeOf) and creates the resource. Reproducing the refusal means the emulator must declare which project exists, which removes the multi-project behaviour it was built with, so it is a product decision rather than a patch. Recorded 2026-08-21 against a real fr-par account (#390).", + "issue": "https://github.com/stephrobert/feint/issues/391" }, { - "file": "outscale/oapi-cli-lb-shapes.jsonl", - "operation": "osc/Client.DeleteNet", + "file": "scaleway/scw-refusals.jsonl", + "operation": "vpcgw/v2/API.CreateGateway", "kind": "absent", - "path": "Errors", - "reason": "A delete is instantaneous here and asynchronous upstream, so the two disagree on every read of a teardown. MEASURED AGAIN on 2026-08-24 and at much greater length than #354 could (#427): this recording's teardown retries until a read proves the object gone, so it carries 26 DeleteLoadBalancer attempts and 24 DeleteNet ones, each answered by the cloud with a 400 or a 409 while the object settled and by this emulator with a 200 at the first ask. The 'absent' findings on the same operations are the same cause seen from the body: an Errors array where the cloud refused, or an empty list where the cloud still held the object. Nothing here is a field the pack fails to serve.", - "issue": "https://github.com/stephrobert/feint/issues/380" + "path": "resource", + "reason": "The create names a project identifier that does not exist and the cloud refuses it before allocating anything; this emulator accepts any project as an isolation boundary (projectOf, scopeOf) and creates the resource. Reproducing the refusal means the emulator must declare which project exists, which removes the multi-project behaviour it was built with, so it is a product decision rather than a patch. Recorded 2026-08-21 against a real fr-par account (#390).", + "issue": "https://github.com/stephrobert/feint/issues/391" }, { - "file": "outscale/oapi-cli-lb-shapes.jsonl", - "operation": "osc/Client.ReadNets", + "file": "scaleway/scw-refusals.jsonl", + "operation": "vpcgw/v2/API.CreateGateway", "kind": "absent", - "path": "Nets[]", - "reason": "A field a real cloudgouv-eu-west-1 account answers and this emulator omits, recorded 2026-08-24 (#427). An image a client cuts from a machine carries its own device mapping and a FileLocation; a volume resize answers the TaskId that will finish it; a health check carries its Path. #437 carries the measured list and the reason each is a distinct decision.", - "issue": "https://github.com/stephrobert/feint/issues/437" + "path": "resource_id", + "reason": "The create names a project identifier that does not exist and the cloud refuses it before allocating anything; this emulator accepts any project as an isolation boundary (projectOf, scopeOf) and creates the resource. Reproducing the refusal means the emulator must declare which project exists, which removes the multi-project behaviour it was built with, so it is a product decision rather than a patch. Recorded 2026-08-21 against a real fr-par account (#390).", + "issue": "https://github.com/stephrobert/feint/issues/391" }, { - "file": "outscale/oapi-cli-lb-shapes.jsonl", - "operation": "osc/Client.UpdateLoadBalancer", + "file": "scaleway/scw-refusals.jsonl", + "operation": "vpcgw/v2/API.CreateGateway", "kind": "status", "path": "", - "reason": "A delete is instantaneous here and asynchronous upstream, so the two disagree on every read of a teardown. MEASURED AGAIN on 2026-08-24 and at much greater length than #354 could (#427): this recording's teardown retries until a read proves the object gone, so it carries 26 DeleteLoadBalancer attempts and 24 DeleteNet ones, each answered by the cloud with a 400 or a 409 while the object settled and by this emulator with a 200 at the first ask. The 'absent' findings on the same operations are the same cause seen from the body: an Errors array where the cloud refused, or an empty list where the cloud still held the object. Nothing here is a field the pack fails to serve.", - "issue": "https://github.com/stephrobert/feint/issues/380" + "reason": "The create names a project identifier that does not exist and the cloud refuses it before allocating anything; this emulator accepts any project as an isolation boundary (projectOf, scopeOf) and creates the resource. Reproducing the refusal means the emulator must declare which project exists, which removes the multi-project behaviour it was built with, so it is a product decision rather than a patch. Recorded 2026-08-21 against a real fr-par account (#390).", + "issue": "https://github.com/stephrobert/feint/issues/391" }, { - "file": "outscale/oapi-cli-lb-shapes.jsonl", - "operation": "osc/Client.UpdateLoadBalancer", + "file": "scaleway/scw-refusals.jsonl", + "operation": "vpcgw/v2/API.CreateIP", "kind": "absent", - "path": "LoadBalancer", - "reason": "A delete is instantaneous here and asynchronous upstream, so the two disagree on every read of a teardown. MEASURED AGAIN on 2026-08-24 and at much greater length than #354 could (#427): this recording's teardown retries until a read proves the object gone, so it carries 26 DeleteLoadBalancer attempts and 24 DeleteNet ones, each answered by the cloud with a 400 or a 409 while the object settled and by this emulator with a 200 at the first ask. The 'absent' findings on the same operations are the same cause seen from the body: an Errors array where the cloud refused, or an empty list where the cloud still held the object. Nothing here is a field the pack fails to serve.", - "issue": "https://github.com/stephrobert/feint/issues/380" + "path": "message", + "reason": "The create names a project identifier that does not exist and the cloud refuses it before allocating anything; this emulator accepts any project as an isolation boundary (projectOf, scopeOf) and creates the resource. Reproducing the refusal means the emulator must declare which project exists, which removes the multi-project behaviour it was built with, so it is a product decision rather than a patch. Recorded 2026-08-21 against a real fr-par account (#390).", + "issue": "https://github.com/stephrobert/feint/issues/391" }, { - "file": "outscale/oapi-cli-lb-shapes.jsonl", - "operation": "osc/Client.DeleteLoadBalancer", + "file": "scaleway/scw-refusals.jsonl", + "operation": "vpcgw/v2/API.CreateIP", "kind": "absent", - "path": "LoadBalancer", - "reason": "A delete is instantaneous here and asynchronous upstream, so the two disagree on every read of a teardown. MEASURED AGAIN on 2026-08-24 and at much greater length than #354 could (#427): this recording's teardown retries until a read proves the object gone, so it carries 26 DeleteLoadBalancer attempts and 24 DeleteNet ones, each answered by the cloud with a 400 or a 409 while the object settled and by this emulator with a 200 at the first ask. The 'absent' findings on the same operations are the same cause seen from the body: an Errors array where the cloud refused, or an empty list where the cloud still held the object. Nothing here is a field the pack fails to serve.", - "issue": "https://github.com/stephrobert/feint/issues/380" + "path": "resource", + "reason": "The create names a project identifier that does not exist and the cloud refuses it before allocating anything; this emulator accepts any project as an isolation boundary (projectOf, scopeOf) and creates the resource. Reproducing the refusal means the emulator must declare which project exists, which removes the multi-project behaviour it was built with, so it is a product decision rather than a patch. Recorded 2026-08-21 against a real fr-par account (#390).", + "issue": "https://github.com/stephrobert/feint/issues/391" }, { - "file": "outscale/oapi-cli-lb-shapes.jsonl", - "operation": "osc/Client.DeleteLoadBalancer", + "file": "scaleway/scw-refusals.jsonl", + "operation": "vpcgw/v2/API.CreateIP", "kind": "absent", - "path": "LoadBalancer.HealthCheck.Path", - "reason": "A delete is instantaneous here and asynchronous upstream, so the two disagree on every read of a teardown. MEASURED AGAIN on 2026-08-24 and at much greater length than #354 could (#427): this recording's teardown retries until a read proves the object gone, so it carries 26 DeleteLoadBalancer attempts and 24 DeleteNet ones, each answered by the cloud with a 400 or a 409 while the object settled and by this emulator with a 200 at the first ask. The 'absent' findings on the same operations are the same cause seen from the body: an Errors array where the cloud refused, or an empty list where the cloud still held the object. Nothing here is a field the pack fails to serve.", - "issue": "https://github.com/stephrobert/feint/issues/380" + "path": "resource_id", + "reason": "The create names a project identifier that does not exist and the cloud refuses it before allocating anything; this emulator accepts any project as an isolation boundary (projectOf, scopeOf) and creates the resource. Reproducing the refusal means the emulator must declare which project exists, which removes the multi-project behaviour it was built with, so it is a product decision rather than a patch. Recorded 2026-08-21 against a real fr-par account (#390).", + "issue": "https://github.com/stephrobert/feint/issues/391" }, { - "file": "outscale/oapi-cli-lb-shapes.jsonl", - "operation": "osc/Client.DeleteLoadBalancer", - "kind": "status", - "path": "", - "reason": "A delete is instantaneous here and asynchronous upstream, so the two disagree on every read of a teardown. MEASURED AGAIN on 2026-08-24 and at much greater length than #354 could (#427): this recording's teardown retries until a read proves the object gone, so it carries 26 DeleteLoadBalancer attempts and 24 DeleteNet ones, each answered by the cloud with a 400 or a 409 while the object settled and by this emulator with a 200 at the first ask. The 'absent' findings on the same operations are the same cause seen from the body: an Errors array where the cloud refused, or an empty list where the cloud still held the object. Nothing here is a field the pack fails to serve.", - "issue": "https://github.com/stephrobert/feint/issues/380" + "file": "scaleway/scw-refusals.jsonl", + "operation": "vpcgw/v2/API.CreateIP", + "kind": "absent", + "path": "type", + "reason": "The create names a project identifier that does not exist and the cloud refuses it before allocating anything; this emulator accepts any project as an isolation boundary (projectOf, scopeOf) and creates the resource. Reproducing the refusal means the emulator must declare which project exists, which removes the multi-project behaviour it was built with, so it is a product decision rather than a patch. Recorded 2026-08-21 against a real fr-par account (#390).", + "issue": "https://github.com/stephrobert/feint/issues/391" }, { - "file": "outscale/oapi-cli-machine-shapes.jsonl", - "operation": "osc/Client.UpdateNet", + "file": "scaleway/scw-refusals.jsonl", + "operation": "vpcgw/v2/API.CreateIP", "kind": "status", "path": "", - "reason": "A delete is instantaneous here and asynchronous upstream, so the two disagree on every read of a teardown. MEASURED AGAIN on 2026-08-24 and at much greater length than #354 could (#427): this recording's teardown retries until a read proves the object gone, so it carries 26 DeleteLoadBalancer attempts and 24 DeleteNet ones, each answered by the cloud with a 400 or a 409 while the object settled and by this emulator with a 200 at the first ask. The 'absent' findings on the same operations are the same cause seen from the body: an Errors array where the cloud refused, or an empty list where the cloud still held the object. Nothing here is a field the pack fails to serve.", - "issue": "https://github.com/stephrobert/feint/issues/380" + "reason": "The create names a project identifier that does not exist and the cloud refuses it before allocating anything; this emulator accepts any project as an isolation boundary (projectOf, scopeOf) and creates the resource. Reproducing the refusal means the emulator must declare which project exists, which removes the multi-project behaviour it was built with, so it is a product decision rather than a patch. Recorded 2026-08-21 against a real fr-par account (#390).", + "issue": "https://github.com/stephrobert/feint/issues/391" }, { - "file": "outscale/oapi-cli-machine-shapes.jsonl", - "operation": "osc/Client.UpdateNet", + "file": "scaleway/terraform.jsonl", + "operation": "vpc/v2/API.CreatePrivateNetwork", "kind": "absent", - "path": "Net", - "reason": "A delete is instantaneous here and asynchronous upstream, so the two disagree on every read of a teardown. MEASURED AGAIN on 2026-08-24 and at much greater length than #354 could (#427): this recording's teardown retries until a read proves the object gone, so it carries 26 DeleteLoadBalancer attempts and 24 DeleteNet ones, each answered by the cloud with a 400 or a 409 while the object settled and by this emulator with a 200 at the first ask. The 'absent' findings on the same operations are the same cause seen from the body: an Errors array where the cloud refused, or an empty list where the cloud still held the object. Nothing here is a field the pack fails to serve.", - "issue": "https://github.com/stephrobert/feint/issues/380" + "path": "has_s3_integration", + "reason": "THE RECORDING PREDATES THE RENAME, AND ONLY AN ACCOUNT CAN RE-RECORD IT. Scaleway renamed the vpc/v2 Object Storage family on 2026-08-25: the VPC's s3_integration_enabled became object_storage_private_access_enabled and the Private Network's has_s3_integration became has_object_storage_private_access. Both upstream sources agree and neither declares a deprecation alias, read on 2026-08-28: .upstream/scaleway-sdk-go/api/vpc/v2/vpc_sdk.go:1024 and :646, and .upstream/scaleway-openapi/vpc-v2.yml, whose x-properties-order -- the document's own exhaustive property list for each object -- carries the new name and not the old one. So the emulator answers the new name alone; answering both would invent a deprecation window no source declares. This corpus was recorded on 2026-08-20, five days before, so it is evidence of what the cloud answered BEFORE the rename and it still settles the two things a rename does not touch: the field is present on every answer, and its value is false on an account with nothing attached. Re-recording needs a paid vpc/v2 account this repository does not have, and `mise run corpus:cloud` is what would arbitrate it. It goes the day that recording is made. (#570)", + "issue": "https://github.com/stephrobert/feint/issues/570" }, { - "file": "outscale/oapi-cli-machine-shapes.jsonl", - "operation": "osc/Client.CreateImage", + "file": "scaleway/terraform.jsonl", + "operation": "vpc/v2/API.CreateVPC", "kind": "absent", - "path": "Image.BlockDeviceMappings[]", - "reason": "An image cut from a VmId answers an empty mapping array where the real account answers the device it snapshotted. Measured 2026-08-24 against cloudgouv-eu-west-1 (#427). It is NOT declined in DeclinedFields, and that is the decision: the same operation DOES serve the mappings when the client names a snapshot, so an operation-level decline would be true of one kind of object and fiction for the other \u2014 the shape #389 cost a release to understand. Serving it means cutting a snapshot of a machine's root volume, which this emulator holds no bytes for. Its neighbour Image.FileLocation left this file on 2026-08-24: it is unserved on every path, so it could be declined honestly.", - "issue": "https://github.com/stephrobert/feint/issues/437" + "path": "s3_integration_enabled", + "reason": "THE RECORDING PREDATES THE RENAME, AND ONLY AN ACCOUNT CAN RE-RECORD IT. Scaleway renamed the vpc/v2 Object Storage family on 2026-08-25: the VPC's s3_integration_enabled became object_storage_private_access_enabled and the Private Network's has_s3_integration became has_object_storage_private_access. Both upstream sources agree and neither declares a deprecation alias, read on 2026-08-28: .upstream/scaleway-sdk-go/api/vpc/v2/vpc_sdk.go:1024 and :646, and .upstream/scaleway-openapi/vpc-v2.yml, whose x-properties-order -- the document's own exhaustive property list for each object -- carries the new name and not the old one. So the emulator answers the new name alone; answering both would invent a deprecation window no source declares. This corpus was recorded on 2026-08-20, five days before, so it is evidence of what the cloud answered BEFORE the rename and it still settles the two things a rename does not touch: the field is present on every answer, and its value is false on an account with nothing attached. Re-recording needs a paid vpc/v2 account this repository does not have, and `mise run corpus:cloud` is what would arbitrate it. It goes the day that recording is made. (#570)", + "issue": "https://github.com/stephrobert/feint/issues/570" }, { - "file": "outscale/oapi-cli-machine-shapes.jsonl", - "operation": "osc/Client.UpdateImage", + "file": "scaleway/terraform.jsonl", + "operation": "vpc/v2/API.GetPrivateNetwork", "kind": "absent", - "path": "Image.BlockDeviceMappings[]", - "reason": "An image cut from a VmId answers an empty mapping array where the real account answers the device it snapshotted. Measured 2026-08-24 against cloudgouv-eu-west-1 (#427). It is NOT declined in DeclinedFields, and that is the decision: the same operation DOES serve the mappings when the client names a snapshot, so an operation-level decline would be true of one kind of object and fiction for the other \u2014 the shape #389 cost a release to understand. Serving it means cutting a snapshot of a machine's root volume, which this emulator holds no bytes for. Its neighbour Image.FileLocation left this file on 2026-08-24: it is unserved on every path, so it could be declined honestly.", - "issue": "https://github.com/stephrobert/feint/issues/437" - }, - { - "file": "outscale/oapi-cli-machine-shapes.jsonl", - "operation": "osc/Client.LinkPrivateIps", - "kind": "status", - "path": "", - "reason": "A delete is instantaneous here and asynchronous upstream, so the two disagree on every read of a teardown. MEASURED AGAIN on 2026-08-24 and at much greater length than #354 could (#427): this recording's teardown retries until a read proves the object gone, so it carries 26 DeleteLoadBalancer attempts and 24 DeleteNet ones, each answered by the cloud with a 400 or a 409 while the object settled and by this emulator with a 200 at the first ask. The 'absent' findings on the same operations are the same cause seen from the body: an Errors array where the cloud refused, or an empty list where the cloud still held the object. Nothing here is a field the pack fails to serve.", - "issue": "https://github.com/stephrobert/feint/issues/380" + "path": "has_s3_integration", + "reason": "THE RECORDING PREDATES THE RENAME, AND ONLY AN ACCOUNT CAN RE-RECORD IT. Scaleway renamed the vpc/v2 Object Storage family on 2026-08-25: the VPC's s3_integration_enabled became object_storage_private_access_enabled and the Private Network's has_s3_integration became has_object_storage_private_access. Both upstream sources agree and neither declares a deprecation alias, read on 2026-08-28: .upstream/scaleway-sdk-go/api/vpc/v2/vpc_sdk.go:1024 and :646, and .upstream/scaleway-openapi/vpc-v2.yml, whose x-properties-order -- the document's own exhaustive property list for each object -- carries the new name and not the old one. So the emulator answers the new name alone; answering both would invent a deprecation window no source declares. This corpus was recorded on 2026-08-20, five days before, so it is evidence of what the cloud answered BEFORE the rename and it still settles the two things a rename does not touch: the field is present on every answer, and its value is false on an account with nothing attached. Re-recording needs a paid vpc/v2 account this repository does not have, and `mise run corpus:cloud` is what would arbitrate it. It goes the day that recording is made. (#570)", + "issue": "https://github.com/stephrobert/feint/issues/570" }, { - "file": "outscale/oapi-cli-machine-shapes.jsonl", - "operation": "osc/Client.UnlinkPrivateIps", - "kind": "status", - "path": "", - "reason": "A delete is instantaneous here and asynchronous upstream, so the two disagree on every read of a teardown. MEASURED AGAIN on 2026-08-24 and at much greater length than #354 could (#427): this recording's teardown retries until a read proves the object gone, so it carries 26 DeleteLoadBalancer attempts and 24 DeleteNet ones, each answered by the cloud with a 400 or a 409 while the object settled and by this emulator with a 200 at the first ask. The 'absent' findings on the same operations are the same cause seen from the body: an Errors array where the cloud refused, or an empty list where the cloud still held the object. Nothing here is a field the pack fails to serve.", - "issue": "https://github.com/stephrobert/feint/issues/380" + "file": "scaleway/terraform.jsonl", + "operation": "vpc/v2/API.GetVPC", + "kind": "absent", + "path": "s3_integration_enabled", + "reason": "THE RECORDING PREDATES THE RENAME, AND ONLY AN ACCOUNT CAN RE-RECORD IT. Scaleway renamed the vpc/v2 Object Storage family on 2026-08-25: the VPC's s3_integration_enabled became object_storage_private_access_enabled and the Private Network's has_s3_integration became has_object_storage_private_access. Both upstream sources agree and neither declares a deprecation alias, read on 2026-08-28: .upstream/scaleway-sdk-go/api/vpc/v2/vpc_sdk.go:1024 and :646, and .upstream/scaleway-openapi/vpc-v2.yml, whose x-properties-order -- the document's own exhaustive property list for each object -- carries the new name and not the old one. So the emulator answers the new name alone; answering both would invent a deprecation window no source declares. This corpus was recorded on 2026-08-20, five days before, so it is evidence of what the cloud answered BEFORE the rename and it still settles the two things a rename does not touch: the field is present on every answer, and its value is false on an account with nothing attached. Re-recording needs a paid vpc/v2 account this repository does not have, and `mise run corpus:cloud` is what would arbitrate it. It goes the day that recording is made. (#570)", + "issue": "https://github.com/stephrobert/feint/issues/570" }, { - "file": "outscale/oapi-cli-machine-shapes.jsonl", - "operation": "osc/Client.ReadVolumes", + "file": "scaleway/terraform.jsonl", + "operation": "vpc/v2/API.UpdatePrivateNetwork", "kind": "absent", - "path": "Volumes[]", - "reason": "A field a real cloudgouv-eu-west-1 account answers and this emulator omits, recorded 2026-08-24 (#427). An image a client cuts from a machine carries its own device mapping and a FileLocation; a volume resize answers the TaskId that will finish it; a health check carries its Path. #437 carries the measured list and the reason each is a distinct decision.", - "issue": "https://github.com/stephrobert/feint/issues/437" + "path": "has_s3_integration", + "reason": "THE RECORDING PREDATES THE RENAME, AND ONLY AN ACCOUNT CAN RE-RECORD IT. Scaleway renamed the vpc/v2 Object Storage family on 2026-08-25: the VPC's s3_integration_enabled became object_storage_private_access_enabled and the Private Network's has_s3_integration became has_object_storage_private_access. Both upstream sources agree and neither declares a deprecation alias, read on 2026-08-28: .upstream/scaleway-sdk-go/api/vpc/v2/vpc_sdk.go:1024 and :646, and .upstream/scaleway-openapi/vpc-v2.yml, whose x-properties-order -- the document's own exhaustive property list for each object -- carries the new name and not the old one. So the emulator answers the new name alone; answering both would invent a deprecation window no source declares. This corpus was recorded on 2026-08-20, five days before, so it is evidence of what the cloud answered BEFORE the rename and it still settles the two things a rename does not touch: the field is present on every answer, and its value is false on an account with nothing attached. Re-recording needs a paid vpc/v2 account this repository does not have, and `mise run corpus:cloud` is what would arbitrate it. It goes the day that recording is made. (#570)", + "issue": "https://github.com/stephrobert/feint/issues/570" }, { - "file": "outscale/oapi-cli-machine-shapes.jsonl", - "operation": "osc/Client.DeleteDhcpOptions", - "kind": "status", - "path": "", - "reason": "A delete is instantaneous here and asynchronous upstream, so the two disagree on every read of a teardown. MEASURED AGAIN on 2026-08-24 and at much greater length than #354 could (#427): this recording's teardown retries until a read proves the object gone, so it carries 26 DeleteLoadBalancer attempts and 24 DeleteNet ones, each answered by the cloud with a 400 or a 409 while the object settled and by this emulator with a 200 at the first ask. The 'absent' findings on the same operations are the same cause seen from the body: an Errors array where the cloud refused, or an empty list where the cloud still held the object. Nothing here is a field the pack fails to serve.", - "issue": "https://github.com/stephrobert/feint/issues/380" + "file": "scaleway/terraform.jsonl", + "operation": "vpc/v2/API.UpdateVPC", + "kind": "absent", + "path": "s3_integration_enabled", + "reason": "THE RECORDING PREDATES THE RENAME, AND ONLY AN ACCOUNT CAN RE-RECORD IT. Scaleway renamed the vpc/v2 Object Storage family on 2026-08-25: the VPC's s3_integration_enabled became object_storage_private_access_enabled and the Private Network's has_s3_integration became has_object_storage_private_access. Both upstream sources agree and neither declares a deprecation alias, read on 2026-08-28: .upstream/scaleway-sdk-go/api/vpc/v2/vpc_sdk.go:1024 and :646, and .upstream/scaleway-openapi/vpc-v2.yml, whose x-properties-order -- the document's own exhaustive property list for each object -- carries the new name and not the old one. So the emulator answers the new name alone; answering both would invent a deprecation window no source declares. This corpus was recorded on 2026-08-20, five days before, so it is evidence of what the cloud answered BEFORE the rename and it still settles the two things a rename does not touch: the field is present on every answer, and its value is false on an account with nothing attached. Re-recording needs a paid vpc/v2 account this repository does not have, and `mise run corpus:cloud` is what would arbitrate it. It goes the day that recording is made. (#570)", + "issue": "https://github.com/stephrobert/feint/issues/570" } ] } diff --git a/docs/limits-acks.json b/docs/limits-acks.json index 5beb6354..aa09b7fc 100644 --- a/docs/limits-acks.json +++ b/docs/limits-acks.json @@ -10,12 +10,12 @@ "A public address is the provider's value, made to answer on the host": "2026-08-27", "A run presented as local can still reach the real cloud (#280)": "2026-08-27", "An API reboot used to log `Failed to add route: file exists` for its own public /32 (#498, lifted 2026-08-27)": "2026-08-27", + "An ERROR is a failure and a WARN is a decline, and one refusal was on the wrong side (#474)": "2026-08-28", "An Exoscale network load balancer records its configuration, names its backends, and grades none of them": "2026-08-27", "An Outscale Vm's options round-trip as data; their behavioural half has nothing to act on here": "2026-08-27", "An Outscale load balancer distributes packets inside its network, and nowhere else": "2026-08-27", "An Outscale machine owns a root volume, and that volume holds no bytes": "2026-08-27", "Exoscale has one zone per process, and the reason is the client": "2026-08-27", - "An ERROR is a failure and a WARN is a decline, and one refusal was on the wrong side (#474)": "2026-08-28", "Identifiers are not checked against anything": "2026-08-27", "Lifecycle transitions are immediate": "2026-08-27", "Managed Kubernetes is not emulated, and a CRUD-only version is refused (#283)": "2026-08-27", @@ -35,6 +35,6 @@ "Two Outscale filters reach the API only as a payload, and that is `octl`'s gap": "2026-08-27", "What survives a dead emulator, in one table": "2026-08-27", "`feint images resolve` can print a `FEINT_BOOT_IMAGES` line that cannot boot (#476)": "2026-08-27", - "`scw` 2.56.3 prints a recovered panic on every successful `lb acl delete`, and the defect is upstream (#505)": "2026-08-27" + "`scw` 2.56.3 prints a recovered panic on every successful `lb acl delete`, and the defect is upstream (#505)": "2026-08-28" } } diff --git a/docs/limits.md b/docs/limits.md index dee6996f..fd82487c 100644 --- a/docs/limits.md +++ b/docs/limits.md @@ -2614,9 +2614,15 @@ real cloud part ways: snapshot for the flag to reveal. The state filter is real code on the default path; the difference from the real cloud, which retains deleted volumes for a while, is this line. -- **`s3_integration_enabled=true` matches nothing.** No VPC or Private Network - here integrates with Object Storage, which is not emulated (see above), so - true truthfully answers an empty list and false answers everything. +- **`object_storage_private_access_enabled=true` matches nothing**, and so does + `s3_integration_enabled=true`, which is the name Scaleway retired on + 2026-08-25 and this filter still accepts (#570): a client that has not been + rebuilt keeps sending it. No VPC or Private Network here integrates with + Object Storage, which is not emulated (see above), so true truthfully answers + an empty list and false answers everything. The *body* answers the new name + alone — both upstream sources declare only that one, and inventing a + deprecation alias in an answer is not the same decision as tolerating a name + a client still sends in a request. - **`arch` and `type` on marketplace ListLocalImages are equalities against the one published image.** `arch=arm64` or `type=instance_local` answers an empty list — the catalogue is x86_64 and `instance_sbs` — where dropping the @@ -3278,11 +3284,13 @@ $ incus query /1.0/instances/feint-scw-d3eaa40c-… | jq -c '.expanded_devices | packet reached the guest and was refused by it, where a covered interface would have dropped it. -**The migration was tried, and it is refused.** #548 left one thing untried — -whether the driver could move the address onto the managed NIC once that one -arrives, which is the shape the other creation order already produces and the -one the rule set covers. It was attempted by hand on 2026-08-27, on the very -machine above, and stopped on two measured facts: +**Two migrations were tried and refused, and a third one works.** #548 left one +thing untried — whether the driver could move the address onto the managed NIC +once that one arrives, which is the shape the other creation order already +produces and the one the rule set covers. Three attempts, and the third is the +reason this paragraph changed on 2026-08-28. + +The first two, by hand on 2026-08-27: ```console $ incus network set feint-uplink ipv4.routes "…,203.0.113.2/32" @@ -3293,16 +3301,69 @@ $ incus query /1.0/instances/feint-scw-d3eaa40c-… | jq -c '…' ``` The uplink cannot be given the `/32` while the routed NIC still owns the host -route for it — the collision #498 documents, met from the other side — so the -address would have to leave the routed NIC first, and there is no ordering -where it is delivered throughout. And removing the routed device unmasks the -profile's `eth0` on `incusbr0`, the operator's own default bridge: the machine -lands on a bridge this emulator refuses to put anything on. A sequence that -gets there exists on paper (mask `eth0` with a `none` device, then delegate, -then set `ipv4.routes.external`, then repair the guest) and it costs a -reconfiguration of the public interface on every private-NIC attach, which -Terraform performs on every apply. That trade was not taken; this paragraph is -the record of the measurement rather than a plan. +route for it — the collision #498 documents, met from the other side — and +removing the routed device unmasks the profile's `eth0` on `incusbr0`, the +operator's own default bridge, which this emulator refuses to put anything on. + +Both come from one place: the host route. **The third attempt takes the address +off the device without taking the device off the instance**, which is neither of +the two, and Incus 7.2 accepts it on a running instance. Measured on 2026-08-28, +under `--vm incus-ovn`, on a server created with its flexible IP whose private +NIC arrived afterwards — the exact shape above: + +```console +$ ip route show | grep 203.0.113.2 +203.0.113.2 dev veth030d08f6 scope link +$ incus config device set feint-scw-21c968ca-… eth0 ipv4.address= # 1 +$ ip route show | grep 203.0.113.2 # gone +$ incus network set feint-uplink ipv4.routes "10.199.0.0/24,203.0.113.2/32" # 2, no longer refused +$ incus config device set feint-scw-21c968ca-… eth1 \ + ipv4.routes.external=203.0.113.2/32 # 3 +$ incus query /1.0/instances/feint-scw-21c968ca-… | jq -c '…' +{"eth0":{"ipv4.host_address":"169.254.0.1","nictype":"routed","type":"nic"}, + "eth1":{"ipv4.address":"10.199.0.2","ipv4.routes.external":"203.0.113.2/32", + "network":"fnt-0da7a7bda1e","security.acls":"scw-892dbc3d91e","type":"nic"}} +``` + +Step 1 releases the address, step 2 is the first refusal now unblocked, step 3 +is `routeAddressOVN`'s own gesture, and the guest is then repaired the way that +function already repairs one. The device stays, so `incusbr0` is never unmasked. + +**The coverage is real, and the probe tells the two answers apart.** The group's +inbound default is `drop` with one rule allowing 443, and a listener sits on both +ports, so a refusal cannot be mistaken for an empty port: + +```console + 203.0.113.2:443 connect_ex=0 OPEN # a rule opens it, the listener answers + 203.0.113.2:80 connect_ex=111 refused # a listener is there, no rule is + 203.0.113.2:8080 connect_ex=111 refused # no listener: the negative control +``` + +Before the migration, on the same machine, 80 was **OPEN** — that is the escape +this section describes. And the machine keeps its way out: `ipv4.nat=true` on the +OVN network, `ping 1.1.1.1` answers from inside, with the station as the control. + +**So this is a remedy, not an impossibility — and it is still not shipped, for +two reasons that are measurements rather than reluctance.** + +- **What a machine "answers on" moves.** `Incus.Inspect` reports the first + global IPv4 of the lowest-named interface, and after the migration the public + and private addresses share `eth1`: the runtime answers + `{"eth0":[],"eth1":["10.199.0.2","203.0.113.2"]}`, stable across three reads, + so `Binding.Address` and `Started.Address` would report the private address + where they used to report the public one. The Scaleway API is unaffected — + it publishes the flexible IP from its own store, and it still answered + `public_ip: 203.0.113.2` throughout — but `Binding.Address` is the shared + layer, and the Exoscale pack reads it (`machines.go`, the membership + attachment). A fix has to say which of a machine's addresses is the one it + answers on, for three packs at once. +- **Only OVN was measured.** The bridge mode delivers a public address through + `ipv4.routes` on the device rather than `ipv4.routes.external`, and nothing + above was run under `--vm incus`. + +Until those two are settled the bound in the table stands, and +`tools/conformance/functional.sh` goes on skipping the public half by naming +`capabilities.firewall_public_only` and #548. What #548 delivered instead is the naming: the refusal now carries every routed interface that escapes *and the addresses it delivers* diff --git a/internal/cli/discipline_test.go b/internal/cli/discipline_test.go index 8a5d8a2a..61521170 100644 --- a/internal/cli/discipline_test.go +++ b/internal/cli/discipline_test.go @@ -131,7 +131,15 @@ var notInTheBarrage = map[string]string{ // declared by its author. A pack that skips one names it in notInTheBarrage with // a reason, which is a line somebody has to write and a reviewer can read. func TestEveryPackRunsTheSharedBarrage(t *testing.T) { - controls := []string{"Sweep", "NoLostUpdate", "Orphans"} + // GoShapes joined on 2026-08-28 (#567), and it is the member that shows why + // the list is discovered rather than declared: the issue stated the three + // real packs were immune because they already store the JSON shape, and the + // first run of this control over Scaleway's own barrage reported 82 + // resources carrying a []string in Attrs["tags"]. Nothing was broken by it, + // because two files carry a hand-written type switch tolerating both + // shapes — which is the habit, and the habit is what a fourth pack cannot + // inherit. + controls := []string{"Sweep", "NoLostUpdate", "Orphans", "GoShapes"} for _, dir := range packDirs(t) { pack := filepath.Base(dir) diff --git a/internal/cli/provider_four_test.go b/internal/cli/provider_four_test.go index 24339175..a400757a 100644 --- a/internal/cli/provider_four_test.go +++ b/internal/cli/provider_four_test.go @@ -896,6 +896,119 @@ func TestTheFourthPacksSpreaderKeepsItsPortAcrossASnapshot(t *testing.T) { } } +// A restored node still wears its barriers, joins its segments and knows its +// addresses (#567). +// +// The numeric half of this is the test above, and it is the half a shared +// reader could fix: a number that crossed JSON has exactly one right answer, +// and resource.Number gives it. These three have none that internal/core may +// give — recovering a []Rule means knowing Rule, which is the pack's own type +// (rule 5) — so the pack stores the shape the door returns instead. +// +// Measured on 2026-08-27, before the fix, through store.Snapshot then +// store.Restore into a fresh store: []Rule came back nil, []string came back +// []any, map[string]string came back map[string]any. Every one of the pack's +// readers asserted the Go type, so a restored node wore no barriers, joined no +// segments and had no address, while the API went on describing all three. +// +// Behaviour rather than shape, deliberately: this asserts what the runtime is +// asked to do with the restored node, not what its Attrs look like. A test +// that compared the maps would pass on a pack that stored the right shape and +// read it back with the wrong assertion. +func TestTheFourthPacksNodeKeepsWhatItWearsAcrossASnapshot(t *testing.T) { + ctx := context.Background() + pack, _, env := fourthPack(t) + + home, err := pack.CreateSegment(ctx, "front", "10.40.0.0/24", "green") + must(t, err) + later, err := pack.CreateSegment(ctx, "back", "10.41.0.0/24", "green") + must(t, err) + barrier := pack.CreateBarrier("web") + must(t, pack.AddRule(ctx, barrier.ID, providerfour.Rule{ + Direction: "ingress", Action: "allow", Protocol: "tcp", Source: "0.0.0.0/0", + PortFrom: 443, PortTo: 443, + })) + node, err := pack.CreateNode(ctx, providerfour.NodeRequest{ + Name: "web-1", + Image: "four-linux", + HomeSegment: home.ID, + Barriers: []string{barrier.ID}, + }) + must(t, err) + must(t, pack.StartNode(ctx, node.ID)) + must(t, pack.JoinSegment(ctx, node.ID, later.ID)) + + // The door a snapshot really travels: the format is documented as meant to + // outlive its instance and be loaded into another one. + var saved bytes.Buffer + if err := env.Store.Snapshot(&saved); err != nil { + t.Fatalf("snapshot: %v", err) + } + restored := store.New() + if err := restored.Restore(bytes.NewReader(saved.Bytes())); err != nil { + t.Fatalf("restore: %v", err) + } + + // A reboot on the revived pack replays the whole post-boot order, which is + // what reads all four stored collections at once. + rec := machine.NewRecorder() + next := fourthEnv(t, machine.Use(rec)) + next.Store = restored + revived := providerfour.New(next) + must(t, revived.RebootNode(ctx, node.ID)) + + var booted machine.Spec + var attached []machine.Attachment + var binding machine.FirewallBinding + var spec machine.FirewallSpec + for _, event := range rec.Events() { + switch event.Kind { + case "Start": + booted, _ = event.Args.(machine.Spec) + case "Attach": + if att, ok := event.Args.(machine.Attachment); ok { + attached = append(attached, att) + } + case "ApplyFirewall": + binding, _ = event.Args.(machine.FirewallBinding) + case "EnsureFirewall": + spec, _ = event.Args.(machine.FirewallSpec) + } + } + + // addresses: the home segment rides the launch, at the address the store + // promised for it. + if len(booted.Attachments) != 1 || booted.Attachments[0].Address != "10.40.0.10" { + t.Errorf("the revived node booted with %v, want one interface at 10.40.0.10: a restored "+ + "map[string]string read as map[string]string is empty, so the interface comes up "+ + "with no address while the API still publishes one", booted.Attachments) + } + + // segments: and the membership joined afterwards is joined again. + if len(attached) != 1 || attached[0].Address != "10.41.0.10" { + t.Errorf("the revived node was attached to %v, want the second segment at 10.41.0.10: a "+ + "restored []string read as []string is nil, so the node joins nothing while the API "+ + "still lists its segments", attached) + } + + // barriers: the rule set the node wears reaches the runtime with it. + if len(binding.Names) != 1 { + t.Errorf("the revived node was bound to %d rule set(s), want 1: a restored []string read "+ + "as []string is nil, so the machine wears nothing while the API still says it does "+ + "— %v", len(binding.Names), binding.Names) + } + + // rules: and the set itself still carries what was declared into it. + if len(spec.Rules) != 1 { + t.Fatalf("the revived rule set carries %d rule(s), want 1: a restored []Rule read as "+ + "[]Rule is nil, so an empty set is handed over under a name the API describes as "+ + "filtering — %v", len(spec.Rules), spec.Rules) + } + if spec.Rules[0].PortFrom != 443 || spec.Rules[0].Source != "0.0.0.0/0" { + t.Errorf("the revived rule is %+v, want the 443 rule that was declared", spec.Rules[0]) + } +} + // ---- The fourth pack is a resource like any other --------------------------- // The fourth pack's own store use obeys the shared write-back. @@ -933,7 +1046,9 @@ func TestTheFourthPacksNestedAttributesAreNeverWrittenThroughTheStore(t *testing if !found { t.Fatal("the node is gone") } - held, _ := stored.Attrs["addresses"].(map[string]string) + // map[string]any, because that is the shape the pack stores since #567 — + // what this measures is the map's aliasing, not its element type. + held, _ := stored.Attrs["addresses"].(map[string]any) if len(held) != 1 { t.Fatalf("the node carries %d address(es) on one segment, want 1: %v", len(held), held) } @@ -949,7 +1064,7 @@ func TestTheFourthPacksNestedAttributesAreNeverWrittenThroughTheStore(t *testing if !found { t.Fatal("the node is gone") } - if addresses, _ := after.Attrs["addresses"].(map[string]string); len(addresses) != 2 { + if addresses, _ := after.Attrs["addresses"].(map[string]any); len(addresses) != 2 { t.Errorf("the store holds %d address(es) after a second segment was joined", len(addresses)) } } diff --git a/internal/cli/testdata/provider-four/intents.go b/internal/cli/testdata/provider-four/intents.go index 3356ca3a..c320069d 100644 --- a/internal/cli/testdata/provider-four/intents.go +++ b/internal/cli/testdata/provider-four/intents.go @@ -134,7 +134,7 @@ type Rule struct { func (p *Pack) CreateBarrier(name string) *resource.Resource { res := resource.New(p.env.NewID(), KindBarrier, tenant(), StateUp, p.env.Now()) res.Attrs["name"] = name - res.Attrs["rules"] = []Rule{} + res.Attrs["rules"] = []any{} p.env.Store.Put(res) return res } @@ -161,7 +161,7 @@ func (p *Pack) AddRule(ctx context.Context, barrierID string, rule Rule) error { next := make([]Rule, 0, len(previous)+1) next = append(next, previous...) next = append(next, rule) - res.Attrs["rules"] = next + res.Attrs["rules"] = rulesAttr(next) if !p.env.Store.Commit(base, res, p.env.Now()) { return ErrNoSuchResource } @@ -313,9 +313,9 @@ func (p *Pack) CreateNode(ctx context.Context, req NodeRequest) (*resource.Resou res := resource.New(p.env.NewID(), KindNode, tenant(), StateDown, p.env.Now()) res.Attrs["name"] = req.Name res.Attrs["image"] = req.Image - res.Attrs["barriers"] = append([]string(nil), req.Barriers...) - res.Attrs["segments"] = []string{} - res.Attrs["addresses"] = map[string]string{} + res.Attrs["barriers"] = stringsAttr(req.Barriers) + res.Attrs["segments"] = []any{} + res.Attrs["addresses"] = map[string]any{} res.Attrs["user_data"] = req.UserData if req.HomeSegment != "" { @@ -328,7 +328,7 @@ func (p *Pack) CreateNode(ctx context.Context, req NodeRequest) (*resource.Resou return nil, fmt.Errorf("four: the segment %s has no address left", segment.ID) } res.Attrs["home_segment"] = req.HomeSegment - res.Attrs["addresses"] = map[string]string{req.HomeSegment: address} + res.Attrs["addresses"] = addressesAttr(map[string]string{req.HomeSegment: address}) } p.env.Store.Put(res) @@ -513,8 +513,8 @@ func (p *Pack) JoinSegment(ctx context.Context, nodeID, segmentID string) error } base := res.Clone() - res.Attrs["segments"] = appendUnique(membershipsOf(res), segmentID) - res.Attrs["addresses"] = withAddress(addressesOf(res), segmentID, address) + res.Attrs["segments"] = stringsAttr(appendUnique(membershipsOf(res), segmentID)) + res.Attrs["addresses"] = addressesAttr(withAddress(addressesOf(res), segmentID, address)) if !p.env.Store.Commit(base, res, p.env.Now()) { return ErrNoSuchResource } @@ -538,8 +538,8 @@ func (p *Pack) LeaveSegment(ctx context.Context, nodeID, segmentID string) error return ErrNoSuchResource } base := res.Clone() - res.Attrs["segments"] = without(membershipsOf(res), segmentID) - res.Attrs["addresses"] = withoutAddress(addressesOf(res), segmentID) + res.Attrs["segments"] = stringsAttr(without(membershipsOf(res), segmentID)) + res.Attrs["addresses"] = addressesAttr(withoutAddress(addressesOf(res), segmentID)) if !p.env.Store.Commit(base, res, p.env.Now()) { return ErrNoSuchResource } @@ -651,7 +651,7 @@ func (p *Pack) CreateSpreader(ctx context.Context, name, segmentID string, port res.Attrs["name"] = name res.Attrs["segment"] = segmentID res.Attrs["port"] = port - res.Attrs["backends"] = []string{} + res.Attrs["backends"] = []any{} p.env.Store.Put(res) p.deliver(ctx, res) return res, nil @@ -668,7 +668,7 @@ func (p *Pack) RegisterBackend(ctx context.Context, spreaderID, nodeID string) e return ErrNoSuchResource } base := res.Clone() - res.Attrs["backends"] = appendUnique(backendsOf(res), nodeID) + res.Attrs["backends"] = stringsAttr(appendUnique(backendsOf(res), nodeID)) if !p.env.Store.Commit(base, res, p.env.Now()) { return ErrNoSuchResource } @@ -792,6 +792,89 @@ func (p *Pack) allocate(segment *resource.Resource, nodeID string) (string, bool // ---- Reading what the store holds ------------------------------------------- +// ---- What goes into Attrs is what a snapshot gives back (#567) ------------- + +// Attrs is a map[string]any and the store's snapshot is JSON, so a restore +// hands back only what encoding/json produces: nil, a bool, a string, a +// float64, a []any, a map[string]any. Every other Go type is a value this pack +// would stop holding the first time somebody runs `feint snapshot load` or +// `PUT /_feint/state`. +// +// This pack wrote the Go shape on its first draft — []Rule, []string, +// map[string]string — and measured the price on 2026-08-27, through +// store.Snapshot then store.Restore into a fresh store: +// +// []Rule (barriers) -> nil +// []string (segments) -> []any +// map[string]string (addresses) -> map[string]any +// +// A restored node therefore wore no barriers, joined no segments, and a +// spreader had no backends, while the API went on describing all three. So the +// Go types stay in this pack's own signatures, where they are useful, and the +// four writers below are the only door into Attrs. storetest.GoShapes is what +// refuses the gesture across every pack, this one included, and internal/cli's +// TestTheFourthPacksNodeKeepsWhatItWearsAcrossASnapshot fails without them. + +// rulesAttr stores a rule set as the array of objects a snapshot returns. +// +// The keys are this pack's wire names, chosen here and nowhere else: a []Rule +// has no recovery internal/core may perform, because recovering one means +// knowing Rule (rule 5). That is the whole difference from a stored number, +// which resource.Number repairs for every pack there will ever be. +func rulesAttr(rules []Rule) []any { + out := make([]any, 0, len(rules)) + for _, rule := range rules { + out = append(out, map[string]any{ + "direction": rule.Direction, + "action": rule.Action, + "protocol": rule.Protocol, + "source": rule.Source, + "source_barrier": rule.SourceBarrier, + "port_from": rule.PortFrom, + "port_to": rule.PortTo, + }) + } + return out +} + +// stringsAttr stores a list of identifiers as the array a snapshot returns. +func stringsAttr(values []string) []any { + out := make([]any, 0, len(values)) + for _, value := range values { + out = append(out, value) + } + return out +} + +// addressesAttr stores a segment-to-address table as the object a snapshot +// returns. +func addressesAttr(addresses map[string]string) map[string]any { + out := make(map[string]any, len(addresses)) + for segmentID, address := range addresses { + out[segmentID] = address + } + return out +} + +// stringsOf reads a stored list of identifiers back. +func stringsOf(v any) []string { + stored, _ := v.([]any) + out := make([]string, 0, len(stored)) + for _, item := range stored { + if s, ok := item.(string); ok { + out = append(out, s) + } + } + return out +} + +// textOf reads a stored string back, answering "" for anything else — the same +// answer the comma-ok assertion it replaces gave. +func textOf(v any) string { + s, _ := v.(string) + return s +} + func attrString(res *resource.Resource, key string) string { if res == nil { return "" @@ -801,18 +884,32 @@ func attrString(res *resource.Resource, key string) string { } func rulesOf(res *resource.Resource) []Rule { - rules, _ := res.Attrs["rules"].([]Rule) + stored, _ := res.Attrs["rules"].([]any) + rules := make([]Rule, 0, len(stored)) + for _, item := range stored { + fields, ok := item.(map[string]any) + if !ok { + continue + } + rules = append(rules, Rule{ + Direction: textOf(fields["direction"]), + Action: textOf(fields["action"]), + Protocol: textOf(fields["protocol"]), + Source: textOf(fields["source"]), + SourceBarrier: textOf(fields["source_barrier"]), + PortFrom: int(resource.Number(fields["port_from"])), + PortTo: int(resource.Number(fields["port_to"])), + }) + } return rules } func wornBarriers(res *resource.Resource) []string { - ids, _ := res.Attrs["barriers"].([]string) - return ids + return stringsOf(res.Attrs["barriers"]) } func membershipsOf(res *resource.Resource) []string { - ids, _ := res.Attrs["segments"].([]string) - return ids + return stringsOf(res.Attrs["segments"]) } // portOf reads back the port a spreader answers on. @@ -835,16 +932,16 @@ func portOf(res *resource.Resource) int { } func backendsOf(res *resource.Resource) []string { - ids, _ := res.Attrs["backends"].([]string) - return ids + return stringsOf(res.Attrs["backends"]) } func addressesOf(res *resource.Resource) map[string]string { - addresses, _ := res.Attrs["addresses"].(map[string]string) - if addresses == nil { - return map[string]string{} + stored, _ := res.Attrs["addresses"].(map[string]any) + out := make(map[string]string, len(stored)) + for segmentID, address := range stored { + out[segmentID] = textOf(address) } - return addresses + return out } // The four helpers below all copy before writing, for one reason: diff --git a/internal/core/resource/resource.go b/internal/core/resource/resource.go index 5e879855..8333d692 100644 --- a/internal/core/resource/resource.go +++ b/internal/core/resource/resource.go @@ -23,6 +23,43 @@ type Tenant struct { // Attrs is the provider-shaped body: the pack owns its keys and the core only // ever copies it. State is kept out of Attrs because the core needs it for // lifecycle transitions, and packs mirror it into Attrs when they serialize. +// +// # What may go into Attrs +// +// The JSON shape, never the Go shape (#567). The store's snapshot is JSON — +// the door `feint snapshot load` and `PUT /_feint/state` both go through, and +// the one snapshot.go documents as meant to outlive its instance — so a +// restore hands back only what encoding/json produces: +// +// nil bool string float64 []any map[string]any +// +// Write anything else and the value silently becomes something else the first +// time a snapshot is taken. Measured on 2026-08-27 on the pack built to be +// copied, internal/cli/testdata/provider-four: +// +// []Rule -> nil a node wearing no rule set +// []string -> []any a node joining no segment +// map[string]string -> map[string]any a node with no address +// +// while the API went on describing all three. And it is not only a newcomer's +// mistake: the first run of the control below over Scaleway's own barrage, +// 2026-08-28, reported 82 resources holding a []string in Attrs["tags"]. +// Nothing was broken by those, because two files in that pack carry a +// hand-written type switch tolerating both shapes — which is the habit, and a +// habit is exactly what the next pack cannot inherit. +// +// Numbers are the one exception, and they are an exception because they have +// an answer: an int, an int64 and a uint32 all come back float64, and Number, +// Int, Int64 and Uint64 in attrs.go read them back for every pack there will +// ever be (#542). A []Rule has no such answer — recovering one means knowing +// Rule, which is the pack's type and must not enter internal/core (rule 5) — +// so the core repairs the number and refuses the shape. +// +// The rule is checked rather than only written down: +// storetest.GoShapes reports every stored value a snapshot cannot give back as +// itself, every pack runs it over its own barrage, and internal/cli's +// TestEveryPackRunsTheSharedBarrage is what makes a fourth pack fail a control +// it never had to remember to write. type Resource struct { ID string Kind string diff --git a/internal/core/store/storetest/goshapes.go b/internal/core/store/storetest/goshapes.go new file mode 100644 index 00000000..ac6fe5b1 --- /dev/null +++ b/internal/core/store/storetest/goshapes.go @@ -0,0 +1,162 @@ +package storetest + +import ( + "encoding/json" + "fmt" + "sort" + + "github.com/stephrobert/feint/internal/core/resource" +) + +// What a pack put in Attrs, judged by what a snapshot can give back (#567). +// +// Attrs is a map[string]any and the store's snapshot is JSON, so what a +// restore hands back is what encoding/json produces and nothing else: nil, a +// bool, a string, a float64, a []any, a map[string]any. A pack that stored any +// other Go type stored something the door cannot return. +// +// Measured on 2026-08-27, on the fourth pack, through store.Snapshot then +// store.Restore into a fresh store: +// +// []Rule (barriers) -> nil +// []string (segments) -> []any +// map[string]string (addresses) -> map[string]any +// +// A restored node therefore wore no barriers, joined no segments, and a +// spreader had no backends, while the API went on describing all three. +// +// Numbers are deliberately not reported, and that boundary is measured rather +// than assumed. An int, an int64 and a uint32 all come back float64 — but a +// number has exactly one right answer on the way back, resource.Number gives +// it, and internal/cli's TestNoPackReadsAStoredNumberByAssertion already +// refuses the gesture that ignores it (#542). A []Rule has no such answer: +// recovering one means knowing the pack's own type, which internal/core must +// not (rule 5). So the shared layer repairs the number and refuses the shape. +// +// It lives here, beside Sweep, for the reason Sweep does: the invariant is the +// core's, the traffic is the pack's. A pack drives it over its own barrage — +// internal/cli's TestEveryPackRunsTheSharedBarrage is what makes a fourth pack +// fail a control it never had to remember to write, which is the half of #567 +// that survives making the model pack correct. +// +// What it cannot see, stated rather than left to be discovered: it judges what +// a barrage produced, so a write on a path no barrage drives is outside it — +// Sweep's blind spot exactly, and this one has already been paid. The first +// run of this control, on 2026-08-28, reported 82 Scaleway resources holding a +// []string in Attrs["tags"] and said nothing about Exoscale, whose pools, +// block volumes and load balancers stored a map[string]string on paths that +// barrage does not reach. Those were found by reading the packs' sources for +// the same gesture. A pack whose barrage grows inherits the coverage; one +// whose barrage stays narrow inherits the silence. + +// jsonShape reports what a value is, in the vocabulary a JSON decoder answers +// in, and whether that vocabulary contains it at all. +// +// The numeric cases are listed rather than reflected on, so that adding a type +// to this control is a decision somebody writes down. json.Number is here +// because a decoder configured with UseNumber produces one and resource.Number +// reads it; the store's own Restore produces float64. +func jsonShape(v any) (kind string, shaped bool) { + switch v.(type) { + case nil: + return "nil", true + case bool: + return "bool", true + case string: + return "string", true + case float64, float32, + int, int8, int16, int32, int64, + uint, uint8, uint16, uint32, uint64, + json.Number: + // A number, repaired on the way out by resource.Number (#542). + return "number", true + case []any: + return "[]any", true + case map[string]any: + return "map[string]any", true + } + return fmt.Sprintf("%T", v), false +} + +// GoShapes reports every stored value a snapshot cannot give back as itself. +// +// One line per offending value, naming the resource, the path inside Attrs and +// the Go type, sorted so two runs of the same failure read the same. Empty +// means every attribute in the store is written in the shape the door returns. +// +// Nested values are walked, because the defect hides one hop in: a pack that +// stores a map[string]any whose values are []string has written the wrong +// shape just as surely as one that stores the []string directly, and no grep +// on the assignment can see it. +func GoShapes(resources []*resource.Resource) []string { + var found []string + for _, res := range resources { + if res == nil { + continue + } + keys := make([]string, 0, len(res.Attrs)) + for key := range res.Attrs { + keys = append(keys, key) + } + sort.Strings(keys) + for _, key := range keys { + walkShape(res, key, res.Attrs[key], &found) + } + } + sort.Strings(found) + return found +} + +// walkShape reports one value and, when the shape is one a decoder produces, +// everything inside it. +// +// depth is bounded by the data: Attrs holds what a handler decoded or built, +// and neither can be cyclic — a cycle would already have hung json.Marshal in +// Snapshot, which every pack's tests run. +func walkShape(res *resource.Resource, path string, v any, found *[]string) { + kind, shaped := jsonShape(v) + if !shaped { + *found = append(*found, fmt.Sprintf( + "%s/%s %s: Attrs[%s] holds a %s, which a snapshot gives back as %s", + res.Tenant.Provider, res.Kind, res.ID, path, kind, restoredAs(v))) + return + } + switch inner := v.(type) { + case []any: + for i, item := range inner { + walkShape(res, fmt.Sprintf("%s[%d]", path, i), item, found) + } + case map[string]any: + keys := make([]string, 0, len(inner)) + for key := range inner { + keys = append(keys, key) + } + sort.Strings(keys) + for _, key := range keys { + walkShape(res, path+"."+key, inner[key], found) + } + } +} + +// restoredAs says what the door will hand back instead, because the failure is +// only actionable if it names the value the next reader will actually meet. +// +// It asks encoding/json rather than reasoning about it: the answer is whatever +// a marshal-then-unmarshal produces, which is exactly what Snapshot and Restore +// do. A value json cannot marshal at all is worse than a reshaped one and says +// so — Snapshot would fail on it. +func restoredAs(v any) string { + encoded, err := json.Marshal(v) + if err != nil { + return "nothing: json.Marshal refuses it, so Snapshot itself fails" + } + var back any + if err := json.Unmarshal(encoded, &back); err != nil { + return "nothing: the encoding does not decode" + } + kind, _ := jsonShape(back) + if back == nil { + return "nil" + } + return kind +} diff --git a/internal/core/store/storetest/goshapes_test.go b/internal/core/store/storetest/goshapes_test.go new file mode 100644 index 00000000..6225c751 --- /dev/null +++ b/internal/core/store/storetest/goshapes_test.go @@ -0,0 +1,179 @@ +package storetest_test + +import ( + "bytes" + "strings" + "testing" + "time" + + "github.com/stephrobert/feint/internal/core/resource" + "github.com/stephrobert/feint/internal/core/store" + "github.com/stephrobert/feint/internal/core/store/storetest" +) + +// A rule struct of the kind a pack owns, so the report can be checked against +// the shape #567 measured rather than against a stand-in. +type barrier struct { + Direction string + PortFrom int +} + +// Both halves, because a control that reports everything passes every attack +// test and makes the barrage useless — and one that reports nothing reads +// exactly like a repository where every pack stores the JSON shape. + +// The refusing half: the three shapes #567 measured, plus the one that hides a +// hop inside a legitimate container. +func TestGoShapesReportsWhatASnapshotCannotGiveBack(t *testing.T) { + res := &resource.Resource{ + ID: "n-1", + Kind: "node", + Tenant: resource.Tenant{Provider: "four"}, + Attrs: map[string]any{ + "barriers": []barrier{{Direction: "ingress", PortFrom: 443}}, + "segments": []string{"seg-1"}, + "addresses": map[string]string{"seg-1": "10.40.0.10"}, + // One hop in: the container is right and its content is not, which + // is the shape no grep on the assignment can see. + "spreader": map[string]any{"backends": []string{"n-2"}}, + }, + } + + found := storetest.GoShapes([]*resource.Resource{res}) + if len(found) != 4 { + t.Fatalf("want four reports, got %d:\n%s", len(found), strings.Join(found, "\n")) + } + report := strings.Join(found, "\n") + for _, want := range []string{ + "Attrs[barriers] holds a []storetest_test.barrier", + "Attrs[segments] holds a []string, which a snapshot gives back as []any", + "Attrs[addresses] holds a map[string]string, which a snapshot gives back as map[string]any", + "Attrs[spreader.backends] holds a []string", + } { + if !strings.Contains(report, want) { + t.Errorf("the report never says %q, so it does not name what the next reader will meet:\n%s", + want, report) + } + } + // The resource is named, because a report that says only "a []string" sends + // the reader looking through every pack. + if !strings.Contains(report, "four/node n-1") { + t.Errorf("the report names no resource:\n%s", report) + } +} + +// The accepting half, and the larger one: everything a pack legitimately +// writes, including every numeric width, must produce no report at all. +// +// The numbers are the part worth asserting rather than assuming. They really +// do change type across a snapshot — that is #542 — and they are deliberately +// not reported here, because resource.Number reads every one of them back and +// internal/cli's TestNoPackReadsAStoredNumberByAssertion already refuses the +// gesture that ignores it. A control that also reported them would make every +// pack in this repository red for a defect the shared layer has already +// answered. +func TestGoShapesStaysSilentOnEverythingAPackLegitimatelyWrites(t *testing.T) { + res := &resource.Resource{ + ID: "s-1", + Kind: "server", + Tenant: resource.Tenant{Provider: "scaleway"}, + Attrs: map[string]any{ + "name": "web", + "absent": nil, + "on": true, + "off": false, + "anInt": 40, + "anInt64": int64(40), + "aUint32": uint32(40), + "aUint64": uint64(40), + "aFloat": 40.5, + "tags": []any{"a", "b"}, + "nested": map[string]any{"port": 443, "name": "http", "on": true}, + "deep": []any{map[string]any{"rules": []any{map[string]any{"port": 22}}}}, + "emptyAny": []any{}, + "emptyMap": map[string]any{}, + }, + } + if found := storetest.GoShapes([]*resource.Resource{res}); len(found) != 0 { + t.Errorf("a resource written entirely in the JSON shape produced %d report(s), so this "+ + "control would cry wolf on every barrage:\n%s", len(found), strings.Join(found, "\n")) + } + // And a nil member is not a crash: Sweep's population is whatever the store + // handed over. + if found := storetest.GoShapes([]*resource.Resource{nil}); len(found) != 0 { + t.Errorf("a nil resource produced %v", found) + } +} + +// And the report is true: what it says a snapshot gives back is what the +// snapshot gives back. +// +// This is the half that makes the control more than an opinion about Go types. +// The message names a value the next reader will actually meet, so it is +// checked against store.Snapshot then store.Restore — the same door #567 was +// measured through — rather than against a belief about encoding/json. +func TestGoShapesNamesWhatTheDoorReallyReturns(t *testing.T) { + st := store.New() + res := resource.New("n-1", "node", resource.Tenant{Provider: "four"}, "up", time.Unix(0, 0).UTC()) + res.Attrs["barriers"] = []barrier{{Direction: "ingress", PortFrom: 443}} + res.Attrs["segments"] = []string{"seg-1"} + res.Attrs["addresses"] = map[string]string{"seg-1": "10.40.0.10"} + st.Put(res) + + var saved bytes.Buffer + if err := st.Snapshot(&saved); err != nil { + t.Fatalf("snapshot: %v", err) + } + next := store.New() + if err := next.Restore(bytes.NewReader(saved.Bytes())); err != nil { + t.Fatalf("restore: %v", err) + } + back, found := next.Get("four", "node", "n-1") + if !found { + t.Fatal("the restored store lost the resource: nothing below measures anything") + } + + // What the report promises, and what actually came back. + for key, want := range map[string]string{ + "segments": "[]interface {}", + "addresses": "map[string]interface {}", + } { + if got := typeName(back.Attrs[key]); got != want { + t.Errorf("%s came back %s, want %s: the report's wording would be a guess", key, got, want) + } + } + // The named type is the loud one: it does not come back reshaped, it comes + // back as a list of anonymous objects, and the pack's own reader yields + // nil for it. + list, isList := back.Attrs["barriers"].([]any) + if !isList { + t.Fatalf("barriers came back %s", typeName(back.Attrs["barriers"])) + } + if _, stillGo := back.Attrs["barriers"].([]barrier); stillGo { + t.Error("the restored value is still a []barrier: #567 has no subject if this holds") + } + if len(list) != 1 { + t.Fatalf("barriers came back with %d member(s)", len(list)) + } + if _, isObject := list[0].(map[string]any); !isObject { + t.Errorf("a restored rule is %s, not the object the report names", typeName(list[0])) + } +} + +func typeName(v any) string { + switch v.(type) { + case []any: + return "[]interface {}" + case map[string]any: + return "map[string]interface {}" + case []string: + return "[]string" + case map[string]string: + return "map[string]string" + case []barrier: + return "[]barrier" + case nil: + return "nil" + } + return "something else" +} diff --git a/internal/providers/exoscale/barrage_test.go b/internal/providers/exoscale/barrage_test.go index 00f555ea..74be5d91 100644 --- a/internal/providers/exoscale/barrage_test.go +++ b/internal/providers/exoscale/barrage_test.go @@ -149,6 +149,14 @@ func TestAnExoscaleBarrageLeavesTheStoreCoherent(t *testing.T) { strings.Join(found, "\n")) } + // What the store holds must be what a snapshot can give back (#567). A + // []string, a map[string]string or a slice of the pack's own struct type + // crosses store.Snapshot/store.Restore as something else, and the pack goes + // on describing what it no longer holds. + if found := storetest.GoShapes(st.All()); len(found) != 0 { + t.Errorf("the barrage stored %d value(s) a snapshot cannot give back as themselves:\n%s", + len(found), strings.Join(found, "\n")) + } if found := storetest.Sweep(st.All(), nil, nil); len(found) != 0 { t.Errorf("the store is incoherent after the barrage:\n%s", strings.Join(found, "\n")) } diff --git a/internal/providers/exoscale/block.go b/internal/providers/exoscale/block.go index 8424a847..436187ce 100644 --- a/internal/providers/exoscale/block.go +++ b/internal/providers/exoscale/block.go @@ -234,7 +234,7 @@ func (p *Pack) createBlockVolume(w http.ResponseWriter, r *http.Request) { "size": size, // Present and empty rather than absent, the lesson snapshots.go paid // for: `exo` dereferences what the schema declares. - "labels": labelsOrEmpty(req.Labels), + "labels": labelsToAttr(req.Labels), } p.env.Store.Put(res) p.writeOperation(w, p.operationReferring(nounBlockVolume, res.ID)) @@ -242,12 +242,6 @@ func (p *Pack) createBlockVolume(w http.ResponseWriter, r *http.Request) { // labelsOrEmpty keeps a declared map present. A nil map marshals to null, and a // client ranging over null is a client that stops. -func labelsOrEmpty(labels map[string]string) map[string]string { - if labels == nil { - return map[string]string{} - } - return labels -} // listBlockVolumes answers the volumes, narrowed to one instance's when the // client asks: their document declares an instance-id filter on this operation @@ -305,7 +299,7 @@ func (p *Pack) updateBlockVolume(w http.ResponseWriter, r *http.Request) { stored.Attrs["name"] = *req.Name } if req.Labels != nil { - stored.Attrs["labels"] = req.Labels + stored.Attrs["labels"] = labelsToAttr(req.Labels) } return nil }) @@ -545,7 +539,7 @@ func (p *Pack) createBlockSnapshot(w http.ResponseWriter, r *http.Request) { // be fiction a client could act on. "size": size, "volume-size": size, - "labels": labelsOrEmpty(req.Labels), + "labels": labelsToAttr(req.Labels), "block-storage-volume": map[string]any{"id": volume.ID}, } res.Runtime = map[string]string{runtimeBlockVolumeKey: volume.ID} @@ -593,7 +587,7 @@ func (p *Pack) updateBlockSnapshot(w http.ResponseWriter, r *http.Request) { stored.Attrs["name"] = *req.Name } if req.Labels != nil { - stored.Attrs["labels"] = req.Labels + stored.Attrs["labels"] = labelsToAttr(req.Labels) } return nil }) diff --git a/internal/providers/exoscale/lifecycle.go b/internal/providers/exoscale/lifecycle.go index 7405fd6a..ba841a29 100644 --- a/internal/providers/exoscale/lifecycle.go +++ b/internal/providers/exoscale/lifecycle.go @@ -266,6 +266,17 @@ func (p *Pack) updateInstance(w http.ResponseWriter, r *http.Request) { // labelsToAttr stores labels as the map[string]any every attrs consumer // (snapshot round-trip included) expects. +// +// It is the pack's only door for labels since #567, and the reason is that it +// used to have two. block.go carried labelsOrEmpty, the same conversion minus +// the conversion: it answered map[string]string, so a pool, a block volume and +// a load balancer stored a Go map that a snapshot gives back as +// map[string]any, while an instance and a private network — created through +// this one — stored the shape the door returns. One pack, one job, two helpers +// and only one of them right, which is #542's seven copies wearing labels. +// storetest.GoShapes refuses the wrong shape now, so the second helper is gone +// rather than fixed: a second door is a door the next author picks by +// proximity. func labelsToAttr(labels map[string]string) map[string]any { out := make(map[string]any, len(labels)) for k, v := range labels { diff --git a/internal/providers/exoscale/loadbalancers.go b/internal/providers/exoscale/loadbalancers.go index 2d10262e..6dd66862 100644 --- a/internal/providers/exoscale/loadbalancers.go +++ b/internal/providers/exoscale/loadbalancers.go @@ -262,7 +262,7 @@ func (p *Pack) createLoadBalancer(w http.ResponseWriter, r *http.Request) { "name": req.Name, "description": req.Description, "ip": ip, - "labels": labelsOrEmpty(req.Labels), + "labels": labelsToAttr(req.Labels), } p.env.Store.Put(res) p.writeOperation(w, p.operationReferring(nounLoadBalancer, res.ID)) @@ -313,7 +313,7 @@ func (p *Pack) updateLoadBalancer(w http.ResponseWriter, r *http.Request) { stored.Attrs["description"] = req.Description } if req.Labels != nil { - stored.Attrs["labels"] = req.Labels + stored.Attrs["labels"] = labelsToAttr(req.Labels) } return nil }) @@ -364,7 +364,7 @@ func (p *Pack) resetLoadBalancerField(w http.ResponseWriter, r *http.Request) { } err := p.env.Store.Update(Name, kindLoadBalancer, id, func(stored *resource.Resource) error { if field == "labels" { - stored.Attrs["labels"] = map[string]string{} + stored.Attrs["labels"] = map[string]any{} return nil } stored.Attrs["description"] = "" diff --git a/internal/providers/exoscale/pools.go b/internal/providers/exoscale/pools.go index 17a78b41..20e92aff 100644 --- a/internal/providers/exoscale/pools.go +++ b/internal/providers/exoscale/pools.go @@ -251,7 +251,7 @@ func (r poolRequest) attrs(size int64) map[string]any { "description": r.Description, "size": size, "instance-prefix": orDefaultPrefix(r.InstancePrefix), - "labels": labelsOrEmpty(r.Labels), + "labels": labelsToAttr(r.Labels), "disk-size": orDefaultDiskSize(r.DiskSize), "user-data": r.UserData, } @@ -531,7 +531,7 @@ func (p *Pack) updateInstancePool(w http.ResponseWriter, r *http.Request) { stored.Attrs["instance-prefix"] = req.InstancePrefix } if req.Labels != nil { - stored.Attrs["labels"] = req.Labels + stored.Attrs["labels"] = labelsToAttr(req.Labels) } if req.Template != nil { stored.Attrs["template"] = map[string]any{"id": req.Template.ID} @@ -755,7 +755,7 @@ func (p *Pack) resetInstancePoolField(w http.ResponseWriter, r *http.Request) { err := p.env.Store.Update(Name, kindPool, id, func(stored *resource.Resource) error { switch field { case "labels": - stored.Attrs["labels"] = map[string]string{} + stored.Attrs["labels"] = map[string]any{} case "min-available": delete(stored.Attrs, "min-available") default: diff --git a/internal/providers/outscale/barrage_test.go b/internal/providers/outscale/barrage_test.go index 4488e851..2783cd2d 100644 --- a/internal/providers/outscale/barrage_test.go +++ b/internal/providers/outscale/barrage_test.go @@ -167,6 +167,14 @@ func TestAnOutscaleBarrageLeavesTheStoreCoherent(t *testing.T) { strings.Join(found, "\n")) } + // What the store holds must be what a snapshot can give back (#567). A + // []string, a map[string]string or a slice of the pack's own struct type + // crosses store.Snapshot/store.Restore as something else, and the pack goes + // on describing what it no longer holds. + if found := storetest.GoShapes(st.All()); len(found) != 0 { + t.Errorf("the barrage stored %d value(s) a snapshot cannot give back as themselves:\n%s", + len(found), strings.Join(found, "\n")) + } if found := storetest.Sweep(st.All(), outscale.Gone, nil); len(found) != 0 { t.Errorf("the store is incoherent after the barrage:\n%s", strings.Join(found, "\n")) } diff --git a/internal/providers/outscale/nics.go b/internal/providers/outscale/nics.go index 80d0b8e3..0eb955e4 100644 --- a/internal/providers/outscale/nics.go +++ b/internal/providers/outscale/nics.go @@ -314,7 +314,7 @@ func (p *Pack) createNic(w http.ResponseWriter, r *http.Request) { "Tags": []any{}, } if len(req.SecurityGroupIDs) > 0 { - nic.Attrs["SecurityGroupIds"] = req.SecurityGroupIDs + nic.Attrs["SecurityGroupIds"] = idsList(req.SecurityGroupIDs) } p.env.Store.Put(nic) unlock() diff --git a/internal/providers/outscale/securitygroups.go b/internal/providers/outscale/securitygroups.go index 2d421611..7b6bc4f3 100644 --- a/internal/providers/outscale/securitygroups.go +++ b/internal/providers/outscale/securitygroups.go @@ -701,6 +701,23 @@ func (p *Pack) effectiveSecurityGroups(vm *resource.Resource) []any { return out } +// idsList is how a list of identifiers enters Attrs: as the shape a snapshot +// gives back, and never nil (#567). +// +// Attrs crosses encoding/json on every snapshot, so a stored []string comes +// back a []any and the pack's own value changes type behind its readers the +// first time `feint snapshot load` or `PUT /_feint/state` is used. stringsOf +// below was written to survive that, one tolerant reader per shape; this +// refuses the write instead, which is the half a reader cannot supply for a +// reader nobody has written yet. +func idsList(ids []string) []any { + out := make([]any, 0, len(ids)) + for _, id := range ids { + out = append(out, id) + } + return out +} + // stringsOf reads a list of strings whatever it crossed: the handler stores // []string, a snapshot restores []any. func stringsOf(v any) []string { diff --git a/internal/providers/outscale/updates.go b/internal/providers/outscale/updates.go index b06d58da..be5ba24a 100644 --- a/internal/providers/outscale/updates.go +++ b/internal/providers/outscale/updates.go @@ -196,7 +196,7 @@ func (p *Pack) updateNic(w http.ResponseWriter, r *http.Request) { stored.Attrs["Description"] = *req.Description } if req.SecurityGroupIDs != nil { - stored.Attrs["SecurityGroupIds"] = req.SecurityGroupIDs + stored.Attrs["SecurityGroupIds"] = idsList(req.SecurityGroupIDs) } if req.LinkNic != nil && req.LinkNic.DeleteOnVMDeletion != nil { // The attachment lives in the flat keys linkNic writes; the diff --git a/internal/providers/outscale/vms.go b/internal/providers/outscale/vms.go index c8836e56..b428ae24 100644 --- a/internal/providers/outscale/vms.go +++ b/internal/providers/outscale/vms.go @@ -446,7 +446,7 @@ func (p *Pack) allocateVms(req createVmsRequest, count int, now time.Time) ([]*r res.Attrs["UserData"] = req.UserData } if len(req.SecurityGroupIDs) > 0 { - res.Attrs["SecurityGroupIds"] = req.SecurityGroupIDs + res.Attrs["SecurityGroupIds"] = idsList(req.SecurityGroupIDs) } p.env.Store.Put(res) // The root device, cut from the snapshot the image names — the last @@ -600,7 +600,7 @@ func (p *Pack) updateVm(w http.ResponseWriter, r *http.Request) { stored.Attrs["VmType"] = req.VMType } if len(req.SecurityGroupIDs) > 0 { - stored.Attrs["SecurityGroupIds"] = req.SecurityGroupIDs + stored.Attrs["SecurityGroupIds"] = idsList(req.SecurityGroupIDs) } if req.KeypairName != "" { stored.Attrs["KeypairName"] = req.KeypairName diff --git a/internal/providers/scaleway/barrage_test.go b/internal/providers/scaleway/barrage_test.go index b866271e..afe8b9b2 100644 --- a/internal/providers/scaleway/barrage_test.go +++ b/internal/providers/scaleway/barrage_test.go @@ -216,6 +216,14 @@ func TestABarrageLeavesTheStoreCoherent(t *testing.T) { len(refused), strings.Join(firstFew(refused, 5), "\n")) } + // What the store holds must be what a snapshot can give back (#567). A + // []string, a map[string]string or a slice of the pack's own struct type + // crosses store.Snapshot/store.Restore as something else, and the pack goes + // on describing what it no longer holds. + if found := storetest.GoShapes(st.All()); len(found) != 0 { + t.Errorf("the barrage stored %d value(s) a snapshot cannot give back as themselves:\n%s", + len(found), strings.Join(found, "\n")) + } if found := storetest.Sweep(st.All(), nil, nil); len(found) != 0 { t.Errorf("the store is incoherent after the barrage:\n%s", strings.Join(found, "\n")) } diff --git a/internal/providers/scaleway/block.go b/internal/providers/scaleway/block.go index 21cec471..034d8ca5 100644 --- a/internal/providers/scaleway/block.go +++ b/internal/providers/scaleway/block.go @@ -443,7 +443,7 @@ func (p *Pack) updateBlockVolume(w http.ResponseWriter, r *http.Request) { stored.Attrs["size"] = *req.Size } if req.Tags != nil { - stored.Attrs["tags"] = *req.Tags + stored.Attrs["tags"] = orEmpty(*req.Tags) } if req.PerfIops != nil { stored.Attrs["perf_iops"] = *req.PerfIops @@ -674,7 +674,7 @@ func (p *Pack) updateBlockSnapshot(w http.ResponseWriter, r *http.Request) { stored.Attrs["name"] = *req.Name } if req.Tags != nil { - stored.Attrs["tags"] = *req.Tags + stored.Attrs["tags"] = orEmpty(*req.Tags) } stored.Updated = p.env.Now() updated = stored @@ -840,7 +840,7 @@ func (p *Pack) newBlockRootVolume(zone, project, name string, size uint64) *reso Attrs: map[string]any{ "name": name, "project": project, - "tags": []string{}, + "tags": []any{}, "size": size, "zone": zone, "parent_snapshot_id": "", diff --git a/internal/providers/scaleway/declined_fields.go b/internal/providers/scaleway/declined_fields.go index 5c2395c4..68b86dce 100644 --- a/internal/providers/scaleway/declined_fields.go +++ b/internal/providers/scaleway/declined_fields.go @@ -87,6 +87,37 @@ func (p *Pack) DeclinedFields() []emulator.FieldDecline { Path: "bastion_allowed_ips[]", Reason: "a range allowed through a bastion that accepts no connection here, while the three operations that would change the list are themselves declined: publishing one would claim a filter nothing enforces and no client can edit", }, + // The VPC's Object Storage flag under the name it had before + // 2026-08-25, and only under that name (#570). + // + // This is not a field the emulator omits: it serves the same flag, + // with the same false value, on the same door. Scaleway renamed it, + // and the shapes catalogue is a recording of what a real fr-par + // account answered on 2026-08-20 — five days before. Both upstream + // sources agree on the new spelling and neither declares a + // deprecation alias (vpc_sdk.go:1024, and vpc-v2.yml whose + // x-properties-order carries object_storage_private_access_enabled + // and not the old name), so answering both would invent a window no + // source declares. + // + // One entry rather than fourteen, and that is the gate's own rule + // rather than an omission: the offline store holds a default VPC and + // no private network, so ListVPCs is the only one of the fourteen + // recorded vpc/v2 operations whose element shape this gate reaches. + // A decline that excuses nothing fails, so the other thirteen have no + // entry until something compares them. The corpus replay, which does + // compare all of them, carries its own twenty entries in + // corpus/accepted.json with the same reason. + // + // It goes the day vpc/v2 is recorded again against a real account, + // and the gate makes that compulsory: the recording will carry the + // new name, this entry will excuse nothing, and the run will fail + // until somebody deletes it. + { + Operation: "GET /vpc/v2/regions/fr-par/vpcs", + Path: "vpcs[].s3_integration_enabled", + Reason: "the name Scaleway retired on 2026-08-25: the same flag is served on this door as object_storage_private_access_enabled, which is what the SDK and the published document declare, and the recording behind this catalogue predates the rename by five days", + }, { Operation: "ipam/v1/API.ListIPs", Path: "ips[].source.zonal", diff --git a/internal/providers/scaleway/gateways.go b/internal/providers/scaleway/gateways.go index a54855b4..496cbe42 100644 --- a/internal/providers/scaleway/gateways.go +++ b/internal/providers/scaleway/gateways.go @@ -230,7 +230,7 @@ func (p *Pack) createGatewayIP(w http.ResponseWriter, r *http.Request) { // mintGatewayIP allocates an address of the gateway block and builds the // resource. The caller holds the allocation lock and stores the result. -func (p *Pack) mintGatewayIP(zone, project string, tags []string) (*resource.Resource, error) { +func (p *Pack) mintGatewayIP(zone, project string, tags []any) (*resource.Resource, error) { prefix, err := network.ParseCIDR(gatewayBlock) if err != nil { return nil, err @@ -455,7 +455,7 @@ func (p *Pack) createGateway(w http.ResponseWriter, r *http.Request) { } else { // "If not set, the emulator mints one", exactly as upstream creates // and attaches a new address when the request names none. - minted, err := p.mintGatewayIP(zone, project, []string{}) + minted, err := p.mintGatewayIP(zone, project, []any{}) if err != nil { writePrecondition(w, "ip", "", err.Error()) return diff --git a/internal/providers/scaleway/images.go b/internal/providers/scaleway/images.go index 6198896d..0905dbb0 100644 --- a/internal/providers/scaleway/images.go +++ b/internal/providers/scaleway/images.go @@ -84,7 +84,7 @@ func (p *Pack) imageView(zone, id, label string) map[string]any { "project": defaultProject, "public": true, "state": "available", - "tags": []string{}, + "tags": []any{}, "zone": zone, "extra_volumes": map[string]any{}, // An empty STRING, never null (#367). The SDK types it as a value — @@ -411,7 +411,7 @@ func (p *Pack) updateImage(w http.ResponseWriter, r *http.Request) { stored.Attrs["name"] = *req.Name } if req.Tags != nil { - stored.Attrs["tags"] = *req.Tags + stored.Attrs["tags"] = orEmpty(*req.Tags) } stored.Updated = p.env.Now() updated = stored diff --git a/internal/providers/scaleway/ipam.go b/internal/providers/scaleway/ipam.go index 6e2e1c98..d70050e9 100644 --- a/internal/providers/scaleway/ipam.go +++ b/internal/providers/scaleway/ipam.go @@ -784,7 +784,7 @@ func (p *Pack) newIPAMIP(region, project string, address netip.Prefix, nic, pn * "address": address.String(), "project_id": project, "is_ipv6": false, - "tags": []string{}, + "tags": []any{}, "private_network_id": pn.ID, "vpc_id": pn.Attrs["vpc_id"], "subnet_id": subnetIDOf(pn.ID), @@ -813,7 +813,7 @@ func (p *Pack) newHeldIPAMIP(region, project string, address netip.Prefix, pn *r "address": address.String(), "project_id": project, "is_ipv6": false, - "tags": []string{}, + "tags": []any{}, "private_network_id": pn.ID, "vpc_id": pn.Attrs["vpc_id"], "subnet_id": subnetIDOf(pn.ID), diff --git a/internal/providers/scaleway/loadbalancer.go b/internal/providers/scaleway/loadbalancer.go index 39e8948f..e7a01bc9 100644 --- a/internal/providers/scaleway/loadbalancer.go +++ b/internal/providers/scaleway/loadbalancer.go @@ -166,7 +166,7 @@ func (p *Pack) createLBIP(w http.ResponseWriter, r *http.Request) { // mintLBIP allocates an address of the balancer blocks and builds the // resource. The caller holds the allocation lock and stores the result. -func (p *Pack) mintLBIP(zone, project string, isIPv6 bool, tags []string) (*resource.Resource, error) { +func (p *Pack) mintLBIP(zone, project string, isIPv6 bool, tags []any) (*resource.Resource, error) { block := lbBlock if isIPv6 { block = lbV6Block @@ -381,7 +381,7 @@ func (p *Pack) createLB(w http.ResponseWriter, r *http.Request) { // address at all, which is how the provider behaves: ip_ids conflicts // with assign_flexible_ip in its own schema. if len(ipIDs) == 0 && (req.AssignFlexibleIP == nil || *req.AssignFlexibleIP) { - minted, err := p.mintLBIP(zone, project, false, []string{}) + minted, err := p.mintLBIP(zone, project, false, []any{}) if err != nil { writePrecondition(w, "ip", "", err.Error()) return @@ -390,7 +390,7 @@ func (p *Pack) createLB(w http.ResponseWriter, r *http.Request) { ipIDs = append(ipIDs, minted.ID) } if req.AssignFlexibleIPv6 != nil && *req.AssignFlexibleIPv6 { - minted, err := p.mintLBIP(zone, project, true, []string{}) + minted, err := p.mintLBIP(zone, project, true, []any{}) if err != nil { writePrecondition(w, "ip", "", err.Error()) return diff --git a/internal/providers/scaleway/placementgroups.go b/internal/providers/scaleway/placementgroups.go index c8b19aa5..033effcb 100644 --- a/internal/providers/scaleway/placementgroups.go +++ b/internal/providers/scaleway/placementgroups.go @@ -207,7 +207,7 @@ func (p *Pack) setPlacementGroupFull(w http.ResponseWriter, r *http.Request) { if req.Tags != nil { stored.Attrs["tags"] = orEmpty(*req.Tags) } else { - stored.Attrs["tags"] = []string{} + stored.Attrs["tags"] = []any{} } stored.Updated = p.env.Now() updated = stored diff --git a/internal/providers/scaleway/securitygroups.go b/internal/providers/scaleway/securitygroups.go index 8091e76a..9b6a9d5c 100644 --- a/internal/providers/scaleway/securitygroups.go +++ b/internal/providers/scaleway/securitygroups.go @@ -678,7 +678,7 @@ func (p *Pack) newSecurityGroup(zone, project, name, description, inbound, outbo "description": description, "organization": defaultOrganization, "project": project, - "tags": []string{}, + "tags": []any{}, "inbound_default_policy": inbound, "outbound_default_policy": outbound, "enable_default_security": true, diff --git a/internal/providers/scaleway/servers.go b/internal/providers/scaleway/servers.go index 9ca4dae9..fc629a5c 100644 --- a/internal/providers/scaleway/servers.go +++ b/internal/providers/scaleway/servers.go @@ -1491,11 +1491,30 @@ func hasEveryTag(res *resource.Resource, wanted []string) bool { return true } -func orEmpty(tags []string) []string { - if tags == nil { - return []string{} +// orEmpty is how a list of strings enters Attrs: as the shape a snapshot gives +// back, and never null (#567). +// +// Two jobs in one line, and the second is the one that was missing. The empty +// list rather than nil, because every recording of this API answers `"tags": +// []` and a client reading null where the cloud sends an array branches +// differently. And []any rather than []string, because Attrs crosses +// encoding/json on every snapshot: a stored []string comes back a []any, so +// the pack's own value changes type behind the readers' backs the first time +// `feint snapshot load` or `PUT /_feint/state` is used. Measured on +// 2026-08-28: a barrage of this pack left 82 resources — security groups, +// snapshots, volumes and a VPC — carrying a []string in Attrs["tags"]. +// +// Nothing broke, and that is the reason it lasted: hasEveryTag and tagsOf each +// carry a hand-written type switch tolerating both shapes, one per file, which +// is #542's seven copies one storey down. storetest.GoShapes now refuses the +// write instead of asking each reader to survive it, and +// TestABarrageLeavesTheStoreCoherent fails without this. +func orEmpty(tags []string) []any { + out := make([]any, 0, len(tags)) + for _, tag := range tags { + out = append(out, tag) } - return tags + return out } func orDefault(v, fallback string) string { diff --git a/internal/providers/scaleway/snapshots.go b/internal/providers/scaleway/snapshots.go index 4dae86b6..edf2fb05 100644 --- a/internal/providers/scaleway/snapshots.go +++ b/internal/providers/scaleway/snapshots.go @@ -236,7 +236,7 @@ func (p *Pack) updateSnapshot(w http.ResponseWriter, r *http.Request) { stored.Attrs["name"] = *req.Name } if req.Tags != nil { - stored.Attrs["tags"] = *req.Tags + stored.Attrs["tags"] = orEmpty(*req.Tags) } stored.Updated = p.env.Now() updated = stored diff --git a/internal/providers/scaleway/volumes.go b/internal/providers/scaleway/volumes.go index bcfe91ce..5354885f 100644 --- a/internal/providers/scaleway/volumes.go +++ b/internal/providers/scaleway/volumes.go @@ -197,7 +197,7 @@ func (p *Pack) newVolume(zone, project, organization, name, volumeType string, s "organization": organization, "size": size, "volume_type": volumeType, - "tags": []string{}, + "tags": []any{}, "export_uri": nil, "boot": false, }, diff --git a/internal/providers/scaleway/vpc.go b/internal/providers/scaleway/vpc.go index ee78f6f1..9dccb7d3 100644 --- a/internal/providers/scaleway/vpc.go +++ b/internal/providers/scaleway/vpc.go @@ -746,7 +746,7 @@ func (p *Pack) ensureDefaultVPC(region, project string) *resource.Resource { } } res := p.newVPC(region, project, "default", true) - res.Attrs["tags"] = []string{defaultVPCTag} + res.Attrs["tags"] = []any{defaultVPCTag} p.env.Store.Put(res) return res } @@ -781,7 +781,7 @@ func (p *Pack) newVPC(region, project, name string, isDefault bool) *resource.Re "name": name, "project_id": project, "organization_id": defaultOrganization, - "tags": []string{}, + "tags": []any{}, "is_default": isDefault, "routing_enabled": true, // Present from the start so the fields serialize on every read. @@ -1142,16 +1142,40 @@ func (p *Pack) vpcView(res *resource.Resource) map[string]any { "created_at": res.Created.Format(time.RFC3339), "updated_at": res.Updated.Format(time.RFC3339), "private_network_count": len(p.privateNetworksOf(res.ID)), - // Measured on 2026-08-20: the earlier recording of ListVPCs was taken - // on an account holding no VPC, so its element shape was never - // observed and this omission was invisible. It is served for the same - // reason its private-network counterpart is — the five operations that - // could attach an Object Storage endpoint are declined in pack.go — - // and listVPCs already answers `s3_integration_enabled=true` with an - // empty list, so the flag and the filter now say the same thing. + // Two measurements, of two different things, and keeping them apart + // is the whole of #570. + // + // *That* the field is carried, and that its value is false, was + // measured on 2026-08-20 against a real fr-par account: the earlier + // recording of ListVPCs was taken on an account holding no VPC, so + // its element shape was never observed and the omission was + // invisible. It is served for the same reason its private-network + // counterpart is — the five operations that could attach an Object + // Storage endpoint are declined in pack.go — and listVPCs already + // answers this filter with an empty list, so the flag and the filter + // say the same thing. + // + // *What it is called* is not that recording's to say any more. + // Scaleway renamed the family on 2026-08-25, and both upstream + // sources agree on the new name, read on 2026-08-28: + // + // .upstream/scaleway-sdk-go/api/vpc/v2/vpc_sdk.go:1024 + // ObjectStoragePrivateAccessEnabled `json:"object_storage_private_access_enabled"` + // .upstream/scaleway-openapi/vpc-v2.yml:796 + // scaleway.vpc.v2.VPC.object_storage_private_access_enabled + // + // The new name alone, and that is a reading of the document rather + // than a taste: `x-properties-order` (vpc-v2.yml:800-814) is the + // document's own exhaustive list of this object's properties, and it + // carries the new name and not the old. Nothing upstream declares a + // deprecation alias, so answering both would be inventing a field no + // source declares — which is the one thing rule 4 forbids. The query + // parameter is the other half and keeps accepting both spellings + // (listquery.go), because a client that has not been rebuilt still + // sends the old one. // // TestTheObjectStorageFlagsAreServedOnEveryDoor fails without it. - "s3_integration_enabled": false, + "object_storage_private_access_enabled": false, } for k, v := range res.Attrs { out[k] = v @@ -1172,13 +1196,23 @@ func privateNetworkView(res *resource.Resource) map[string]any { // /vpc/v2/regions/fr-par/private-networks/{id}); the emulator omitted // it, and the contract has always declared it. // + // Renamed with its VPC sibling on 2026-08-25 (#570), and the name + // comes from upstream rather than from that recording, read on + // 2026-08-28: + // + // .upstream/scaleway-sdk-go/api/vpc/v2/vpc_sdk.go:646 + // HasObjectStoragePrivateAccess `json:"has_object_storage_private_access"` + // .upstream/scaleway-openapi/vpc-v2.yml:552, and its + // x-properties-order at :556-569, which carries the new name and + // not the old one + // // Computed here rather than stored, and always false: it says an Object // Storage endpoint is attached, and the five operations that could // attach one are declined in pack.go with their reason. A stored flag // would be a value nothing can ever change. // // TestTheObjectStorageFlagsAreServedOnEveryDoor fails without it. - "has_s3_integration": false, + "has_object_storage_private_access": false, } for k, v := range res.Attrs { if k == "subnet" || k == "subnet_ipv6" { diff --git a/internal/providers/scaleway/vpc_measured_test.go b/internal/providers/scaleway/vpc_measured_test.go index 5fd87d65..4bf170ac 100644 --- a/internal/providers/scaleway/vpc_measured_test.go +++ b/internal/providers/scaleway/vpc_measured_test.go @@ -78,18 +78,44 @@ func TestBookIPAnswersWhatTheRealCloudAnswers(t *testing.T) { } } -// The two Object Storage flags are served, on every door. +// The two Object Storage flags are served, on every door, under the names +// upstream publishes today (#352, renamed by #570). // // They are false and can only be false here: the five operations that attach a // private network to Object Storage are declined in pack.go with their reason. -// That is not an argument for omitting them — a client reading -// `has_s3_integration` off a decoded object gets the zero value either way, and -// a client comparing field sets does not. The contract has always declared -// both; nothing observed them until a Private Network and a VPC existed at the -// moment of a recording. +// That is not an argument for omitting them — a client reading the flag off a +// decoded object gets the zero value either way, and a client comparing field +// sets does not. The contract declares both; nothing observed them until a +// Private Network and a VPC existed at the moment of a recording. // // Every door, because the emulator has more than one: a create, a read, and a // list, and the list is the one a previous omission survived in. +// +// # What the 2026-08-20 recording still arbitrates, and what it no longer does +// +// This file grades against a capture taken from a real fr-par account on +// 2026-08-20, and Scaleway renamed this family on 2026-08-25. The recording is +// therefore historical for one of the two things it used to settle, and saying +// which is the whole point of writing it here rather than moving the divergence +// into the gate: +// +// - it still settles that the field is **present on every answer** and that +// its **value is false** on an account with no Object Storage endpoint +// attached. A rename does not touch either, and nothing else in this +// repository establishes them; +// - it no longer settles the **spelling**. That comes from upstream, read on +// 2026-08-28: vpc_sdk.go:1024 and :646, and the published document +// .upstream/scaleway-openapi/vpc-v2.yml, whose x-properties-order — the +// document's own exhaustive property list — carries +// object_storage_private_access_enabled and +// has_object_storage_private_access, and neither old name. +// +// Re-recording would need a paid account, which this repository does not have +// for vpc/v2 after 2026-08-20, so the split above is the honest form: the +// assertion below names the new fields, and corpus/scaleway/terraform.jsonl +// keeps answering with the old ones. That disagreement is real, it is declared +// in the corpus acceptance list with this reason and this date, and it is what +// `mise run corpus:cloud` would arbitrate the day somebody has an account. func TestTheObjectStorageFlagsAreServedOnEveryDoor(t *testing.T) { ts := newTestServer(t) @@ -112,13 +138,27 @@ func TestTheObjectStorageFlagsAreServedOnEveryDoor(t *testing.T) { } } - present("CreateVPC", createdVPC, "s3_integration_enabled") - present("CreatePrivateNetwork", createdPN, "has_s3_integration") + // And the old spelling is gone rather than kept beside the new one. No + // upstream source declares a deprecation alias — the SDK has one field and + // the document's x-properties-order has one entry — so a body carrying + // both would be a shape this emulator invented, which is the one thing a + // response body may never be. + absent := func(what string, body map[string]any, field string) { + t.Helper() + if _, carried := body[field]; carried { + t.Errorf("%s still carries %s, renamed by Scaleway on 2026-08-25; nothing upstream "+ + "declares it any more, and a field no source declares is a field this emulator "+ + "invented", what, field) + } + } + + present("CreateVPC", createdVPC, "object_storage_private_access_enabled") + present("CreatePrivateNetwork", createdPN, "has_object_storage_private_access") _, readVPC := do(t, ts, "GET", vpcRegion+"/vpcs/"+vpcID, "") - present("GetVPC", readVPC, "s3_integration_enabled") + present("GetVPC", readVPC, "object_storage_private_access_enabled") _, readPN := do(t, ts, "GET", vpcRegion+"/private-networks/"+pnID, "") - present("GetPrivateNetwork", readPN, "has_s3_integration") + present("GetPrivateNetwork", readPN, "has_object_storage_private_access") _, listedVPCs := do(t, ts, "GET", vpcRegion+"/vpcs", "") vpcs, _ := listedVPCs["vpcs"].([]any) @@ -127,7 +167,7 @@ func TestTheObjectStorageFlagsAreServedOnEveryDoor(t *testing.T) { } for _, raw := range vpcs { entry, _ := raw.(map[string]any) - present("ListVPCs", entry, "s3_integration_enabled") + present("ListVPCs", entry, "object_storage_private_access_enabled") } _, listedPNs := do(t, ts, "GET", vpcRegion+"/private-networks", "") @@ -137,8 +177,13 @@ func TestTheObjectStorageFlagsAreServedOnEveryDoor(t *testing.T) { } for _, raw := range networks { entry, _ := raw.(map[string]any) - present("ListPrivateNetworks", entry, "has_s3_integration") + present("ListPrivateNetworks", entry, "has_object_storage_private_access") } + + absent("CreateVPC", createdVPC, "s3_integration_enabled") + absent("GetVPC", readVPC, "s3_integration_enabled") + absent("CreatePrivateNetwork", createdPN, "has_s3_integration") + absent("GetPrivateNetwork", readPN, "has_s3_integration") } // The default VPC carries tags ["default"], and one a client creates carries diff --git a/tools/falsify/specs/attrs-json-shape.json b/tools/falsify/specs/attrs-json-shape.json new file mode 100644 index 00000000..8b73df73 --- /dev/null +++ b/tools/falsify/specs/attrs-json-shape.json @@ -0,0 +1,72 @@ +{ + "package": "./internal/core/store/storetest/", + "mutations": [ + { + "label": "the shape control accepts every Go type, so a barrage of packs storing []Rule, []string and map[string]string sweeps clean and reports a repository where nothing is lost across a snapshot", + "file": "internal/core/store/storetest/goshapes.go", + "find": "\treturn fmt.Sprintf(\"%T\", v), false\n}", + "replace": "\treturn fmt.Sprintf(\"%T\", v), true\n}", + "test": "TestGoShapesReportsWhatASnapshotCannotGiveBack" + }, + { + "label": "the walk stops at the top level, so a Go shape one hop inside a legitimate map is invisible — which is the half no grep on the assignment can see", + "file": "internal/core/store/storetest/goshapes.go", + "find": "\t\tfor _, key := range keys {\n\t\t\twalkShape(res, path+\".\"+key, inner[key], found)\n\t\t}", + "replace": "\t\tfor _, key := range keys[:0] {\n\t\t\twalkShape(res, path+\".\"+key, inner[key], found)\n\t\t}", + "test": "TestGoShapesReportsWhatASnapshotCannotGiveBack" + }, + { + "label": "THE SECOND DIRECTION: a []any is reported as a Go shape, so the control refuses the very shape it exists to require and every barrage goes red", + "file": "internal/core/store/storetest/goshapes.go", + "find": "\tcase []any:\n\t\treturn \"[]any\", true", + "replace": "\tcase []any:\n\t\treturn \"[]any\", false", + "test": "TestGoShapesStaysSilentOnEverythingAPackLegitimatelyWrites" + }, + { + "label": "THE SECOND DIRECTION: a stored number is reported, so the control contradicts resource.Number and #542's accepted remedy on every pack", + "file": "internal/core/store/storetest/goshapes.go", + "find": "\t\treturn \"number\", true", + "replace": "\t\treturn \"number\", false", + "test": "TestGoShapesStaysSilentOnEverythingAPackLegitimatelyWrites" + }, + { + "label": "the report guesses instead of asking: what it says a snapshot gives back stops coming from a marshal-and-decode, and becomes an opinion about Go types", + "file": "internal/core/store/storetest/goshapes.go", + "find": "\tencoded, err := json.Marshal(v)", + "replace": "\tencoded, err := json.Marshal(\"a guess\")\n\t_ = v", + "test": "TestGoShapesReportsWhatASnapshotCannotGiveBack" + }, + { + "label": "Scaleway puts a Go shape back inside Attrs[\"tags\"], which is the 82-resource defect the first run of this control measured on 2026-08-28. Planted inside the list rather than as the list, because orEmpty answers []any by signature and the barrage sends no tag: a mutation on the loop body never executed, and reported the guard as unmeasured", + "file": "internal/providers/scaleway/servers.go", + "find": "func orEmpty(tags []string) []any {\n\tout := make([]any, 0, len(tags))", + "replace": "func orEmpty(tags []string) []any {\n\tout := make([]any, 0, len(tags))\n\tout = append(out, []string(nil))", + "test": "TestABarrageLeavesTheStoreCoherent", + "package": "./internal/providers/scaleway/" + }, + { + "label": "the model pack reads its identifier lists the Go way again, so a restored node wears no barrier and joins no segment while the API describes both", + "file": "internal/cli/testdata/provider-four/intents.go", + "find": "\tstored, _ := v.([]any)\n\tout := make([]string, 0, len(stored))", + "replace": "\tstored, _ := v.([]any)\n\tstored = stored[:0]\n\tout := make([]string, 0, len(stored))", + "test": "TestTheFourthPacksNodeKeepsWhatItWearsAcrossASnapshot", + "package": "./internal/cli/" + }, + { + "label": "the model pack reads its address table the Go way again, so a restored node boots with no address on an interface the API publishes one for", + "file": "internal/cli/testdata/provider-four/intents.go", + "find": "\tstored, _ := res.Attrs[\"addresses\"].(map[string]any)\n\tout := make(map[string]string, len(stored))", + "replace": "\tstored, _ := res.Attrs[\"addresses\"].(map[string]any)\n\tstored = nil\n\tout := make(map[string]string, len(stored))", + "test": "TestTheFourthPacksNodeKeepsWhatItWearsAcrossASnapshot", + "package": "./internal/cli/" + }, + { + "label": "the model pack reads its rule set the Go way again, so an empty set is handed to the runtime under a name the API describes as filtering", + "file": "internal/cli/testdata/provider-four/intents.go", + "find": "\tstored, _ := res.Attrs[\"rules\"].([]any)\n\trules := make([]Rule, 0, len(stored))", + "replace": "\tstored, _ := res.Attrs[\"rules\"].([]any)\n\tstored = stored[:0]\n\trules := make([]Rule, 0, len(stored))", + "test": "TestTheFourthPacksNodeKeepsWhatItWearsAcrossASnapshot", + "package": "./internal/cli/" + } + ] +} diff --git a/tools/falsify/specs/corpus-findings.json b/tools/falsify/specs/corpus-findings.json index 4af45a9b..8e02ce0f 100644 --- a/tools/falsify/specs/corpus-findings.json +++ b/tools/falsify/specs/corpus-findings.json @@ -4,8 +4,8 @@ { "label": "the default VPC goes back to answering no tags, which is what a fresh emulator did while the real one answered [\"default\"] — the first defect the committed corpus surfaced", "file": "internal/providers/scaleway/vpc.go", - "find": "\tres.Attrs[\"tags\"] = []string{defaultVPCTag}", - "replace": "\tres.Attrs[\"tags\"] = []string{}\n\t_ = defaultVPCTag", + "find": "\tres.Attrs[\"tags\"] = []any{defaultVPCTag}", + "replace": "\tres.Attrs[\"tags\"] = []any{}\n\t_ = defaultVPCTag", "test": "TestTheDefaultVPCCarriesTheDefaultTag" }, { diff --git a/tools/falsify/specs/private-network-ipv6.json b/tools/falsify/specs/private-network-ipv6.json index 3f433feb..3d35bb71 100644 --- a/tools/falsify/specs/private-network-ipv6.json +++ b/tools/falsify/specs/private-network-ipv6.json @@ -31,17 +31,17 @@ "package": "./internal/core/network/" }, { - "label": "the private network stops publishing the Object Storage flag the real cloud carries", + "label": "the private network stops publishing the Object Storage flag the real cloud carries, under the name upstream publishes since 2026-08-25 (#570)", "file": "internal/providers/scaleway/vpc.go", - "find": "\t\t\"has_s3_integration\": false,", - "replace": "\t\t\"has_s3_integration\" + \"-unserved\": false,", + "find": "\t\t\"has_object_storage_private_access\": false,", + "replace": "\t\t\"has_object_storage_private_access\" + \"-unserved\": false,", "test": "TestTheObjectStorageFlagsAreServedOnEveryDoor" }, { - "label": "the VPC stops publishing the Object Storage flag the real cloud carries", + "label": "the VPC stops publishing the Object Storage flag the real cloud carries, under the name upstream publishes since 2026-08-25 (#570)", "file": "internal/providers/scaleway/vpc.go", - "find": "\t\t\"s3_integration_enabled\": false,", - "replace": "\t\t\"s3_integration_enabled\" + \"-unserved\": false,", + "find": "\t\t\"object_storage_private_access_enabled\": false,", + "replace": "\t\t\"object_storage_private_access_enabled\" + \"-unserved\": false,", "test": "TestTheObjectStorageFlagsAreServedOnEveryDoor" }, { @@ -50,6 +50,13 @@ "find": "const vpcCreateStatus = http.StatusOK", "replace": "const vpcCreateStatus = http.StatusCreated", "test": "TestTheVpcCreatesAnswerWhatTheRealCloudAnswers" + }, + { + "label": "THE SECOND DIRECTION (#570): the VPC answers the retired spelling beside the new one, which is the deprecation window no upstream source declares", + "file": "internal/providers/scaleway/vpc.go", + "find": "\t\t\"object_storage_private_access_enabled\": false,", + "replace": "\t\t\"object_storage_private_access_enabled\": false,\n\t\t\"s3_integration_enabled\": false,", + "test": "TestTheObjectStorageFlagsAreServedOnEveryDoor" } ] } diff --git a/tools/falsify/specs/provider-four.json b/tools/falsify/specs/provider-four.json index fecaf808..a4318c68 100644 --- a/tools/falsify/specs/provider-four.json +++ b/tools/falsify/specs/provider-four.json @@ -58,10 +58,10 @@ "test": "TestTheFourthPacksSpreaderRecordsTheDeliveryAndNotTheIntent" }, { - "label": "the fourth pack writes a segment address into the map the store still shares, outside the lock and outside the conditional write-back (#295, #517)", + "label": "the fourth pack writes a segment address into the map the store still shares, outside the lock and outside the conditional write-back (#295, #517). Planted at JoinSegment since #567: withAddress's own copy stopped being the thing that holds this, because addressesOf now decodes a map[string]any into a fresh map[string]string and the conversion IS the copy — mutating withAddress no longer reaches the store's map, and the replay reported STILL GREEN for a property that had simply moved", "file": "internal/cli/testdata/provider-four/intents.go", - "find": "\tout := make(map[string]string, len(addresses)+1)\n\tfor key, value := range addresses {\n\t\tout[key] = value\n\t}\n\tout[segmentID] = address\n\treturn out\n}", - "replace": "\tout := make(map[string]string, len(addresses)+1)\n\tfor key, value := range addresses {\n\t\tout[key] = value\n\t}\n\taddresses[segmentID] = address\n\treturn addresses\n}", + "find": "\tres.Attrs[\"segments\"] = stringsAttr(appendUnique(membershipsOf(res), segmentID))\n\tres.Attrs[\"addresses\"] = addressesAttr(withAddress(addressesOf(res), segmentID, address))", + "replace": "\tres.Attrs[\"segments\"] = stringsAttr(appendUnique(membershipsOf(res), segmentID))\n\tif shared, ok := res.Attrs[\"addresses\"].(map[string]any); ok {\n\t\tshared[segmentID] = address\n\t}\n\tres.Attrs[\"addresses\"] = addressesAttr(withAddress(addressesOf(res), segmentID, address))", "test": "TestTheFourthPacksNestedAttributesAreNeverWrittenThroughTheStore" }, {