From 6a5550b7b0cf5ae80ac31e042a800ca1f1796788 Mon Sep 17 00:00:00 2001 From: Mikhail Date: Sun, 16 Aug 2026 15:51:18 +0300 Subject: [PATCH] feat: show Creator continuity in Domovoy --- cmd/creator-web/main.go | 6 +- cmd/creator-web/server.go | 63 ++++++++++++++----- cmd/creator-web/server_test.go | 31 ++++++++- cmd/creator-web/static/assets/console.css | 19 +++++- cmd/creator-web/static/assets/console.js | 35 ++++++++++- cmd/creator-web/static/index.html | 22 ++++++- deploy/docker-compose.dev.yml | 3 +- deploy/docker-compose.prod.yml | 3 +- ...DEV-000027-creator-continuity-console.json | 17 +++++ tests/creator_web/verify-remote.ps1 | 30 ++++++++- 10 files changed, 201 insertions(+), 28 deletions(-) create mode 100644 dev/tasks/DEV-000027-creator-continuity-console.json diff --git a/cmd/creator-web/main.go b/cmd/creator-web/main.go index 31287f0..589a89b 100644 --- a/cmd/creator-web/main.go +++ b/cmd/creator-web/main.go @@ -9,7 +9,11 @@ import ( func main() { addr := envOr("LISTEN_ADDR", ":8084") - handler, err := newHandler(envOr("CORE_RUNTIME_URL", "http://core-runtime:8080"), &http.Client{Timeout: 10 * time.Second}) + handler, err := newHandler( + envOr("CORE_RUNTIME_URL", "http://core-runtime:8080"), + envOr("CONTINUITY_RUNTIME_URL", "http://continuity-runtime:8081"), + &http.Client{Timeout: 10 * time.Second}, + ) if err != nil { log.Fatal(err) } diff --git a/cmd/creator-web/server.go b/cmd/creator-web/server.go index b1cd8c3..4df28bf 100644 --- a/cmd/creator-web/server.go +++ b/cmd/creator-web/server.go @@ -20,15 +20,27 @@ const ( var staticFiles embed.FS type gateway struct { - upstream *url.URL - client *http.Client - static http.Handler + core *url.URL + continuity *url.URL + client *http.Client + static http.Handler } -func newHandler(upstream string, client *http.Client) (http.Handler, error) { - parsed, err := url.Parse(upstream) - if err != nil || parsed.Scheme == "" || parsed.Host == "" { - return nil, fmt.Errorf("invalid core runtime URL") +type routeTarget uint8 + +const ( + targetCore routeTarget = iota + targetContinuity +) + +func newHandler(coreURL, continuityURL string, client *http.Client) (http.Handler, error) { + core, err := parseRuntimeURL(coreURL, "Core") + if err != nil { + return nil, err + } + continuity, err := parseRuntimeURL(continuityURL, "Continuity") + if err != nil { + return nil, err } assets, err := fs.Sub(staticFiles, "static") if err != nil { @@ -37,7 +49,15 @@ func newHandler(upstream string, client *http.Client) (http.Handler, error) { if client == nil { client = http.DefaultClient } - return securityHeaders(&gateway{upstream: parsed, client: client, static: http.FileServer(http.FS(assets))}), nil + return securityHeaders(&gateway{core: core, continuity: continuity, client: client, static: http.FileServer(http.FS(assets))}), nil +} + +func parseRuntimeURL(rawURL, name string) (*url.URL, error) { + parsed, err := url.Parse(rawURL) + if err != nil || parsed.Scheme == "" || parsed.Host == "" { + return nil, fmt.Errorf("invalid %s Runtime URL", name) + } + return parsed, nil } func (g *gateway) ServeHTTP(w http.ResponseWriter, r *http.Request) { @@ -61,7 +81,7 @@ func (g *gateway) ServeHTTP(w http.ResponseWriter, r *http.Request) { } func (g *gateway) forwardAPI(w http.ResponseWriter, r *http.Request) { - upstreamPath, allowed := creatorRoute(r.Method, r.URL.Path) + routeTarget, upstreamPath, allowed := creatorRoute(r.Method, r.URL.Path) if !allowed { http.Error(w, "creator route denied", http.StatusNotFound) return @@ -75,7 +95,11 @@ func (g *gateway) forwardAPI(w http.ResponseWriter, r *http.Request) { http.Error(w, "request too large", http.StatusRequestEntityTooLarge) return } - target := *g.upstream + upstream := g.core + if routeTarget == targetContinuity { + upstream = g.continuity + } + target := *upstream target.Path = upstreamPath target.RawQuery = r.URL.RawQuery request, err := http.NewRequestWithContext(r.Context(), r.Method, target.String(), strings.NewReader(string(body))) @@ -98,26 +122,31 @@ func (g *gateway) forwardAPI(w http.ResponseWriter, r *http.Request) { _, _ = io.Copy(w, io.LimitReader(response.Body, maxResponseBytes)) } -func creatorRoute(method, path string) (string, bool) { +func creatorRoute(method, path string) (routeTarget, string, bool) { switch { case method == http.MethodPost && path == "/api/session": - return "/v1/creator/sessions", true + return targetCore, "/v1/creator/sessions", true case method == http.MethodGet && path == "/api/session/current": - return "/v1/creator/sessions/current", true + return targetCore, "/v1/creator/sessions/current", true case method == http.MethodGet && path == "/api/focus": - return "/v1/creator/focus", true + return targetCore, "/v1/creator/focus", true case method == http.MethodGet && strings.HasPrefix(path, "/api/tasks/"): id := strings.TrimPrefix(path, "/api/tasks/") if validID(id) { - return "/v1/creator/tasks/" + id, true + return targetCore, "/v1/creator/tasks/" + id, true } case method == http.MethodGet && strings.HasPrefix(path, "/api/capabilities/") && strings.HasSuffix(path, "/health"): id := strings.TrimSuffix(strings.TrimPrefix(path, "/api/capabilities/"), "/health") if validID(id) { - return "/v1/creator/capabilities/" + id + "/health", true + return targetCore, "/v1/creator/capabilities/" + id + "/health", true + } + case method == http.MethodGet && strings.HasPrefix(path, "/api/prospective-items/"): + id := strings.TrimPrefix(path, "/api/prospective-items/") + if validID(id) { + return targetContinuity, "/v1/creator/prospective-items/" + id, true } } - return "", false + return targetCore, "", false } func validID(value string) bool { diff --git a/cmd/creator-web/server_test.go b/cmd/creator-web/server_test.go index e0ceb1d..5a0215f 100644 --- a/cmd/creator-web/server_test.go +++ b/cmd/creator-web/server_test.go @@ -20,7 +20,7 @@ func TestGatewayForwardsOnlyCreatorAllowlist(t *testing.T) { _, _ = io.WriteString(w, `{"TaskID":"task-1"}`) })) defer upstream.Close() - handler, err := newHandler(upstream.URL, upstream.Client()) + handler, err := newHandler(upstream.URL, upstream.URL, upstream.Client()) if err != nil { t.Fatal(err) } @@ -34,7 +34,7 @@ func TestGatewayForwardsOnlyCreatorAllowlist(t *testing.T) { } func TestGatewayDeniesInternalAndMutationRoutes(t *testing.T) { - handler, err := newHandler("http://core-runtime:8080", http.DefaultClient) + handler, err := newHandler("http://core-runtime:8080", "http://continuity-runtime:8081", http.DefaultClient) if err != nil { t.Fatal(err) } @@ -42,6 +42,7 @@ func TestGatewayDeniesInternalAndMutationRoutes(t *testing.T) { {http.MethodPost, "/api/tasks/task-1"}, {http.MethodGet, "/api/principal-bindings/actor/principal"}, {http.MethodPost, "/api/focus-task-commits"}, + {http.MethodPost, "/api/prospective-items/item-1"}, {http.MethodGet, "/api/tasks/../../healthz"}, } { request := httptest.NewRequest(test.method, test.path, nil) @@ -53,8 +54,32 @@ func TestGatewayDeniesInternalAndMutationRoutes(t *testing.T) { } } +func TestGatewayRoutesProspectiveItemToContinuity(t *testing.T) { + continuity := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path != "/v1/creator/prospective-items/item-1" { + t.Fatalf("unexpected Continuity target %s", r.URL.Path) + } + if r.Header.Get("Authorization") != "Bearer bounded-session" { + t.Fatal("authorization header was not forwarded") + } + _, _ = io.WriteString(w, `{"ID":"item-1"}`) + })) + defer continuity.Close() + handler, err := newHandler("http://core-runtime.invalid", continuity.URL, continuity.Client()) + if err != nil { + t.Fatal(err) + } + request := httptest.NewRequest(http.MethodGet, "/api/prospective-items/item-1", nil) + request.Header.Set("Authorization", "Bearer bounded-session") + response := httptest.NewRecorder() + handler.ServeHTTP(response, request) + if response.Code != http.StatusOK || !strings.Contains(response.Body.String(), "item-1") { + t.Fatalf("response = %d %s", response.Code, response.Body.String()) + } +} + func TestConsoleAndSecurityHeaders(t *testing.T) { - handler, err := newHandler("http://core-runtime:8080", http.DefaultClient) + handler, err := newHandler("http://core-runtime:8080", "http://continuity-runtime:8081", http.DefaultClient) if err != nil { t.Fatal(err) } diff --git a/cmd/creator-web/static/assets/console.css b/cmd/creator-web/static/assets/console.css index f181c73..2cc7f8d 100644 --- a/cmd/creator-web/static/assets/console.css +++ b/cmd/creator-web/static/assets/console.css @@ -84,7 +84,7 @@ button:focus-visible, input:focus-visible { outline: 2px solid var(--signal-brig .workspace-head { display: flex; align-items: end; justify-content: space-between; gap: 24px; margin-bottom: 34px; } .workspace-head h1 { margin: 0; font: 400 clamp(36px, 5vw, 58px)/1 var(--serif); letter-spacing: -.035em; } .last-update { color: var(--faint); font: 10px/1.4 var(--mono); text-align: right; } -.signal-strip { display: grid; grid-template-columns: repeat(3, 1fr); margin-bottom: 18px; border: 1px solid var(--line); background: var(--panel); } +.signal-strip { display: grid; grid-template-columns: repeat(4, 1fr); margin-bottom: 18px; border: 1px solid var(--line); background: var(--panel); } .signal-strip div { padding: 16px 18px; display: flex; align-items: center; justify-content: space-between; gap: 12px; border-right: 1px solid var(--line); } .signal-strip div:last-child { border: 0; } .signal-strip span { color: var(--faint); font: 9px/1 var(--mono); letter-spacing: .13em; } @@ -96,7 +96,8 @@ button:focus-visible, input:focus-visible { outline: 2px solid var(--signal-brig .data-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 18px; } .data-card { min-width: 0; padding: 24px; border: 1px solid var(--line); background: linear-gradient(145deg, rgba(22, 31, 27, .94), rgba(13, 20, 17, .98)); box-shadow: 0 10px 40px rgba(0,0,0,.12); animation: reveal .55s both; } .task-card { animation-delay: .08s; } -.capability-card { grid-column: 1 / -1; animation-delay: .16s; } +.prospective-card { grid-column: 1 / -1; animation-delay: .16s; } +.capability-card { grid-column: 1 / -1; animation-delay: .24s; } .data-card header { display: grid; grid-template-columns: 30px 1fr auto; gap: 12px; align-items: start; margin-bottom: 26px; } .card-number { color: var(--signal); font: 10px/1.5 var(--mono); } .data-card header p { margin: 0 0 5px; color: var(--faint); font: 9px/1 var(--mono); letter-spacing: .14em; text-transform: uppercase; } @@ -112,6 +113,15 @@ button:focus-visible, input:focus-visible { outline: 2px solid var(--signal-brig .data-card dt { margin-bottom: 7px; color: var(--faint); font: 9px/1 var(--mono); letter-spacing: .1em; text-transform: uppercase; } .data-card dd { margin: 0; overflow: hidden; color: var(--ink); font: 11px/1.4 var(--mono); text-overflow: ellipsis; white-space: nowrap; } .card-message { min-height: 18px; margin: 16px 0 0; color: var(--muted); font-size: 11px; line-height: 1.5; } +.prospective-core { margin-bottom: 12px; display: grid; grid-template-columns: minmax(0, .8fr) minmax(0, 1.2fr); border-top: 1px solid var(--line-soft); border-bottom: 1px solid var(--line-soft); } +.prospective-core > div { min-width: 0; padding: 22px 0; } +.prospective-core > div:first-child { padding-right: 24px; border-right: 1px solid var(--line-soft); } +.prospective-core > div:last-child { padding-left: 24px; } +.prospective-core span { display: block; color: var(--faint); font: 9px/1 var(--mono); letter-spacing: .13em; } +.prospective-core strong { display: block; margin-top: 8px; font: 400 clamp(30px, 5vw, 45px)/1 var(--serif); } +.time-marker { position: relative; } +.time-marker i { position: absolute; left: 24px; right: 0; bottom: -1px; height: 1px; background: linear-gradient(90deg, var(--signal), transparent); } +.time-marker time { display: block; margin-top: 10px; color: var(--ink); font: 13px/1.4 var(--mono); } .capability-core { display: grid; grid-template-columns: 110px 1fr; gap: 28px; align-items: center; padding: 8px 0 26px; } .capability-core > div:last-child > span { display: block; color: var(--faint); font: 9px/1 var(--mono); letter-spacing: .13em; } .capability-core strong { display: block; margin: 8px 0; font: 400 clamp(30px, 5vw, 46px)/1 var(--serif); } @@ -137,6 +147,7 @@ button:focus-visible, input:focus-visible { outline: 2px solid var(--signal-brig .workspace { padding: 32px 24px; } .data-grid { grid-template-columns: 1fr; } .capability-card { grid-column: auto; } + .prospective-card { grid-column: auto; } } @media (max-width: 680px) { @@ -160,4 +171,8 @@ button:focus-visible, input:focus-visible { outline: 2px solid var(--signal-brig .data-card dl div { padding-right: 0 !important; } .capability-core { grid-template-columns: 76px 1fr; gap: 18px; } .radar { width: 72px; height: 72px; } + .prospective-core { grid-template-columns: 1fr; } + .prospective-core > div:first-child { padding-right: 0; border-right: 0; border-bottom: 1px solid var(--line-soft); } + .prospective-core > div:last-child { padding-left: 0; } + .time-marker i { left: 0; } } diff --git a/cmd/creator-web/static/assets/console.js b/cmd/creator-web/static/assets/console.js index 215013b..d543396 100644 --- a/cmd/creator-web/static/assets/console.js +++ b/cmd/creator-web/static/assets/console.js @@ -5,7 +5,7 @@ const loginForm = byId("login-form"); const contextForm = byId("context-form"); const loginError = byId("login-error"); const toast = byId("toast"); -const contextFields = ["session-id", "workspace-id", "task-id", "capability-id"]; +const contextFields = ["session-id", "workspace-id", "task-id", "prospective-id", "capability-id"]; let sessionToken = sessionStorage.getItem("domovoy.creator.session") || ""; let toastTimer; @@ -145,6 +145,12 @@ async function loadDashboard() { setBadge("task", "НЕ ЗАГРУЖЕНА", "neutral"); setText("task-message", "Укажите задачу для проверки относительно активного фокуса."); } + if (context["prospective-id"]) work.push(loadProspective(context["prospective-id"])); + else { + setSignal("prospective", "не выбрано", "idle"); + setBadge("prospective", "НЕ ЗАГРУЖЕНА", "neutral"); + setText("prospective-message", "Укажите идентификатор ProspectiveItem для проверки владельца и состояния."); + } if (context["capability-id"]) work.push(loadCapability(context["capability-id"])); else setSignal("capability", "не выбрана", "idle"); @@ -189,6 +195,33 @@ async function loadTask(taskID, query) { } } +async function loadProspective(prospectiveID) { + setSignal("prospective", "загрузка", "loading"); + try { + const item = await request(`/api/prospective-items/${encodeURIComponent(prospectiveID)}`); + const state = item.State || "UNKNOWN"; + const goodStates = ["REGISTERED", "ARMED", "SATISFIED"]; + const warningStates = ["TRIGGERED", "REVALIDATING", "DELIVERED"]; + const tone = goodStates.includes(state) ? "good" : warningStates.includes(state) ? "warn" : "bad"; + setText("prospective-state", state); + setText("prospective-trigger", item.TriggerAtUnixMillis ? new Date(item.TriggerAtUnixMillis).toLocaleString("ru-RU") : "не указано"); + setText("prospective-value", item.ID); + setText("prospective-commitment", item.CommitmentID); + setText("prospective-goal", item.GoalID); + setText("prospective-scope", item.ScopeID); + setText("prospective-owner", item.OwnerPrincipalID); + setText("prospective-revision", item.Revision); + setText("prospective-message", "Запись принадлежит текущему Principal и прочитана из канонического Continuity state."); + setSignal("prospective", state.toLowerCase(), tone === "bad" ? "error" : "ok"); + setBadge("prospective", state, tone); + } catch (error) { + setSignal("prospective", "ошибка", "error"); + setBadge("prospective", "НЕДОСТУПНА", "bad"); + setText("prospective-state", "UNKNOWN"); + setText("prospective-message", error.message); + } +} + async function loadCapability(capabilityID) { setSignal("capability", "загрузка", "loading"); try { diff --git a/cmd/creator-web/static/index.html b/cmd/creator-web/static/index.html index fb9b38c..7c250cd 100644 --- a/cmd/creator-web/static/index.html +++ b/cmd/creator-web/static/index.html @@ -60,6 +60,8 @@ + + @@ -84,6 +86,7 @@
ФОКУСожидает
ЗАДАЧАожидает
+
CONTINUITYожидает
ВОЗМОЖНОСТЬожидает
@@ -110,8 +113,25 @@

Укажите задачу для проверки относительно активного фокуса.

+
+
03

Continuity

Будущая значимость

НЕ ЗАГРУЖЕНА
+
+
СОСТОЯНИЕ—
+
СРАБОТАЕТ
+
+
+
Ожидание
—
+
Обязательство
—
+
Цель
—
+
Область
—
+
Principal
—
+
Ревизия
—
+
+

Укажите идентификатор ProspectiveItem для проверки владельца и состояния.

+
+
-
03

Capability

Готовность

НЕИЗВЕСТНО
+
04

Capability

Готовность

НЕИЗВЕСТНО
ТЕКУЩЕЕ СОСТОЯНИЕ—Причина не получена
diff --git a/deploy/docker-compose.dev.yml b/deploy/docker-compose.dev.yml index 1613bd0..ad9e367 100644 --- a/deploy/docker-compose.dev.yml +++ b/deploy/docker-compose.dev.yml @@ -100,8 +100,9 @@ services: environment: LISTEN_ADDR: ":8084" CORE_RUNTIME_URL: http://core-runtime:8080 + CONTINUITY_RUNTIME_URL: http://continuity-runtime:8081 ports: ["19084:8084"] - depends_on: [core-runtime] + depends_on: [core-runtime, continuity-runtime] volumes: assistant_pg_dev: diff --git a/deploy/docker-compose.prod.yml b/deploy/docker-compose.prod.yml index a16aea5..eeecd76 100644 --- a/deploy/docker-compose.prod.yml +++ b/deploy/docker-compose.prod.yml @@ -88,8 +88,9 @@ services: environment: LISTEN_ADDR: ":8084" CORE_RUNTIME_URL: http://core-runtime:8080 + CONTINUITY_RUNTIME_URL: http://continuity-runtime:8081 ports: ["${CREATOR_WEB_PORT:-18080}:8084"] - depends_on: [core-runtime] + depends_on: [core-runtime, continuity-runtime] healthcheck: test: ["CMD-SHELL", "wget -qO- http://localhost:8084/healthz >/dev/null"] interval: 10s diff --git a/dev/tasks/DEV-000027-creator-continuity-console.json b/dev/tasks/DEV-000027-creator-continuity-console.json new file mode 100644 index 0000000..64d7eda --- /dev/null +++ b/dev/tasks/DEV-000027-creator-continuity-console.json @@ -0,0 +1,17 @@ +{ + "task_id": "DEV-000027", + "task_revision": 1, + "architecture_revision": "bootstrap-1", + "base_repo_revision": "eb343cd76eb025949cd0543276930528ad2f2085", + "risk_tier": "C", + "intent": "Expose the authenticated Continuity read endpoint through the narrow Creator gateway and present an owned ProspectiveItem in the Domovoy console.", + "allowed_change_scope": {"paths": ["cmd/creator-web/**", "deploy/docker-compose.prod.yml", "deploy/docker-compose.dev.yml", "tests/creator_web/**", "dev/tasks/DEV-000027-creator-continuity-console.json"]}, + "forbidden_change_scope": {"paths": ["architecture/**", "security/**", "database/**", "modules/**", "contracts/**", "adapters/**", "cmd/core-runtime/**", "cmd/continuity-runtime/**", "cmd/commit-runtime/**"]}, + "allowed_architecture_delta": {"new_cross_module_dependencies": 0, "new_canonical_owners": 0, "new_privileges": 0}, + "contracts": {"consume": ["contracts.continuity@v1"], "may_change": []}, + "semantic_owner": "none-interface-adapter", + "assumptions": ["Continuity Runtime exposes DEV-000026 inside the assistant Docker network", "ProspectiveItem identifiers are explicit operator context and are not credentials"], + "acceptance_scenarios": ["gateway allowlists the Creator ProspectiveItem GET and no Continuity mutation", "console displays state, trigger, owner-safe references and revision", "missing or denied items produce explicit UI state", "desktop and mobile layouts remain usable"], + "required_tests": ["unit", "gateway allowlist", "runtime HTTP", "responsive browser", "architecture", "remote Docker smoke"], + "terminal_gap_states": ["BLOCKED_BY_ARCHITECTURE_GAP", "BLOCKED_BY_SPEC_GAP", "BLOCKED_BY_CONFLICT"] +} diff --git a/tests/creator_web/verify-remote.ps1 b/tests/creator_web/verify-remote.ps1 index 482f092..fbb6587 100644 --- a/tests/creator_web/verify-remote.ps1 +++ b/tests/creator_web/verify-remote.ps1 @@ -5,6 +5,22 @@ param( $ErrorActionPreference = "Stop" $coreContainer = "assistant-core-runtime-1" +$continuityContainer = "assistant-continuity-runtime-1" + +function Post-ContinuityJson([string]$url, [object]$body) { + $json = $body | ConvertTo-Json -Depth 8 -Compress + $local = New-TemporaryFile + $remote = "/tmp/domovoy-$([guid]::NewGuid().ToString('N')).json" + try { + [IO.File]::WriteAllText($local, $json, [Text.UTF8Encoding]::new($false)) + docker --context $DockerContext cp $local "${continuityContainer}:$remote" | Out-Null + docker --context $DockerContext exec $continuityContainer wget -qO- --header "Content-Type: application/json" --post-file $remote $url + if ($LASTEXITCODE -ne 0) { throw "Continuity fixture creation failed" } + } finally { + docker --context $DockerContext exec --user root $continuityContainer rm -f $remote 2>$null + Remove-Item -LiteralPath $local -Force + } +} $health = Invoke-RestMethod -Uri "$BaseURL/healthz" -Method Get if ($health.status -ne "ok" -or $health.runtime -ne "creator-web") { throw "Creator web health response is invalid" } @@ -15,6 +31,8 @@ if (-not $page.Headers["Content-Security-Policy"]) { throw "Creator console secu $denied = Invoke-WebRequest -Uri "$BaseURL/api/focus-task-commits" -Method Post -SkipHttpErrorCheck if ($denied.StatusCode -ne 404) { throw "Internal mutation route was exposed" } +$continuityMutation = Invoke-WebRequest -Uri "$BaseURL/api/prospective-items/blocked" -Method Post -SkipHttpErrorCheck +if ($continuityMutation.StatusCode -ne 404) { throw "Continuity mutation route was exposed" } $invalid = Invoke-WebRequest -Uri "$BaseURL/api/session/current" -Headers @{Authorization="Bearer invalid"} -SkipHttpErrorCheck if ($invalid.StatusCode -ne 401) { throw "Invalid Creator session was not denied" } @@ -30,4 +48,14 @@ if ($current.ActorID -ne "domovoy-creator-actor") { throw "Creator identity was $capability = Invoke-RestMethod -Uri "$BaseURL/api/capabilities/cognitive-proposal/health" -Method Get -Headers @{Authorization="Bearer $($issued.Token)"} if ($capability.CapabilityID -ne "cognitive-proposal") { throw "Capability health was not returned through the gateway" } -Write-Output "DEV-000025 remote Creator web console: PASS" +$suffix = [guid]::NewGuid().ToString("N") +$prospectiveID = "web-prospective-$suffix" +Post-ContinuityJson "http://localhost:8081/v1/prospective-items" @{ + ID=$prospectiveID;CommitmentID="web-commitment-$suffix";OwnerPrincipalID="domovoy-creator-principal" + ScopeID="domovoy-global-scope";GoalID="web-goal-$suffix";TriggerAtUnixMillis=1800000000000 + State="ARMED";Revision=1 +} +$prospective = Invoke-RestMethod -Uri "$BaseURL/api/prospective-items/$prospectiveID" -Method Get -Headers @{Authorization="Bearer $($issued.Token)"} +if ($prospective.ID -ne $prospectiveID -or $prospective.State -ne "ARMED") { throw "ProspectiveItem was not returned through the gateway" } + +Write-Output "DEV-000027 remote Creator Continuity console: PASS"