A session is a consumer of a licence, and (node, module) already names one
Work breakdown 1.2. Two sessions run on the control-plane node — the node's own and the mesh's (novox/hq ADR 0026) — so a machine stopped being a usable answer to "whose licence is this". 14-model-access.md called per-module-per-machine "a step toward it and not it", and that is true of a worker: many run on one machine from one module, so the pair cannot name them apart. It is not true of a session. The two sessions are two modules — the same mechanism started in different context roots, and a context root is what a module delivers — so (node, module) tells them apart and nothing needed adding. Checked rather than argued: different licences on one machine, each with its own key, and a session on no licence is not handed the other's. The third test exists because a fault injection stayed silent. The first two put the sessions on different licences, so the licence alone disambiguates and the module argument is never load-bearing — removing it from the query changed nothing and everything still passed. Two sessions on the SAME licence is the case that needs the pair to be the identity: releasing one must leave the other, and a machine-shaped answer takes both.
This commit is contained in:
@@ -160,3 +160,151 @@ func TestUsingALicenceThatDoesNotExistIsRefused(t *testing.T) {
|
|||||||
t.Fatalf("the refusal does not name the licence: %v", err)
|
t.Fatalf("the refusal does not name the licence: %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Two sessions on one machine, each on its own licence (novox/hq work breakdown 1.2).
|
||||||
|
//
|
||||||
|
// **The case ADR 0026 creates.** The control-plane node runs its own node session and the mesh's,
|
||||||
|
// so a machine is no longer a usable answer to *whose licence is this*. `14-model-access.md`
|
||||||
|
// records that gap as "a consumer that is not a machine", and the question this answers is whether
|
||||||
|
// it needs a new consumer identity or whether the existing one already distinguishes them.
|
||||||
|
//
|
||||||
|
// It does. The two sessions are two modules — the same mechanism started in different context
|
||||||
|
// roots (ADR 0026), and a context root is what a module delivers — so `(node, module)` names them
|
||||||
|
// apart without a schema knowing anything about sessions.
|
||||||
|
func TestTwoSessionsOnOneMachineHoldDifferentLicences(t *testing.T) {
|
||||||
|
held, ctx := fresh(t)
|
||||||
|
for _, l := range []struct{ name, provider string }{
|
||||||
|
{"personal", "anthropic"},
|
||||||
|
{"company", "anthropic"},
|
||||||
|
} {
|
||||||
|
if err := held.Add(ctx, l.name, l.provider, map[string]any{"model": "a-model"}); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Both on the machine that holds the control plane.
|
||||||
|
const machine = "anchor"
|
||||||
|
if err := held.Use(ctx, "personal", machine, "node-session"); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if err := held.Use(ctx, "company", machine, "mesh-session"); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Each is asked for separately, and neither answer is the other's.
|
||||||
|
node, err := held.Chosen(ctx, machine, "node-session")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
mesh, err := held.Chosen(ctx, machine, "mesh-session")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if node != "personal" || mesh != "company" {
|
||||||
|
t.Fatalf("the node session is on %q and the mesh session on %q; expected personal and company",
|
||||||
|
node, mesh)
|
||||||
|
}
|
||||||
|
|
||||||
|
// And the keys are separate too, which is the half that matters: a machine-wide answer would
|
||||||
|
// hand both sessions whichever key was sealed last.
|
||||||
|
sealing := aKey(t)
|
||||||
|
for _, l := range []struct{ name, value string }{
|
||||||
|
{"personal", "sk-the-operators-own-key"},
|
||||||
|
{"company", "sk-the-companys-key"},
|
||||||
|
} {
|
||||||
|
if _, err := held.Accept(ctx, l.name, l.value, func(string) (string, error) {
|
||||||
|
return sealing, nil
|
||||||
|
}); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
first, err := held.KeyFor(ctx, "personal", machine, "node-session")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
second, err := held.KeyFor(ctx, "company", machine, "mesh-session")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if first == "" || second == "" {
|
||||||
|
t.Fatal("a session on a licence was given no key")
|
||||||
|
}
|
||||||
|
if first == second {
|
||||||
|
t.Fatal("both sessions were given the same sealed key, so the machine answered rather " +
|
||||||
|
"than the session")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// A session put on no licence is not silently given the machine's other one.
|
||||||
|
func TestASessionOnNoLicenceIsAnsweredWithNothing(t *testing.T) {
|
||||||
|
held, ctx := fresh(t)
|
||||||
|
if err := held.Add(ctx, "personal", "anthropic", map[string]any{"model": "a-model"}); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if err := held.Use(ctx, "personal", "anchor", "node-session"); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
chosen, err := held.Chosen(ctx, "anchor", "mesh-session")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if chosen != "" {
|
||||||
|
t.Fatalf("a session nobody put on a licence was answered with %q, which belongs to "+
|
||||||
|
"something else on the same machine", chosen)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Two sessions on one machine and the SAME licence are still two holders.
|
||||||
|
//
|
||||||
|
// **Written because a fault injection stayed silent.** The test above puts them on different
|
||||||
|
// licences, so the licence alone tells them apart and the module argument is never load-bearing —
|
||||||
|
// removing it from the query changed nothing and every assertion still passed. This is the case
|
||||||
|
// that needs `(node, module)` to be the identity: releasing one session must leave the other,
|
||||||
|
// and a machine-shaped answer would take both.
|
||||||
|
func TestReleasingOneSessionLeavesTheOtherOnTheSameMachine(t *testing.T) {
|
||||||
|
held, ctx := fresh(t)
|
||||||
|
if err := held.Add(ctx, "company", "anthropic", map[string]any{"model": "a-model"}); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
const machine = "anchor"
|
||||||
|
for _, session := range []string{"node-session", "mesh-session"} {
|
||||||
|
if err := held.Use(ctx, "company", machine, session); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if _, err := held.Accept(ctx, "company", "sk-the-companys-key", func(string) (string, error) {
|
||||||
|
return aKey(t), nil
|
||||||
|
}); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := held.StopUsing(ctx, "company", machine, "node-session"); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
gone, err := held.Chosen(ctx, machine, "node-session")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if gone != "" {
|
||||||
|
t.Errorf("the released session is still on %q", gone)
|
||||||
|
}
|
||||||
|
|
||||||
|
kept, err := held.Chosen(ctx, machine, "mesh-session")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if kept != "company" {
|
||||||
|
t.Fatal("releasing one session took the other's licence with it, so the machine was " +
|
||||||
|
"released rather than the session")
|
||||||
|
}
|
||||||
|
key, err := held.KeyFor(ctx, "company", machine, "mesh-session")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if key == "" {
|
||||||
|
t.Fatal("the session that was kept lost its key")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user