feat: add controlled Amap credential probe
Dada P0-A isolated Windows CI / validate-and-package (push) Failing after 1m0s
Dada P0-A isolated Windows CI / validate-and-package (push) Failing after 1m0s
This commit is contained in:
@@ -28,35 +28,40 @@ const build = run("pnpm", ["build:workspace-packages"]);
|
|||||||
const contract = build.exit_code === 0
|
const contract = build.exit_code === 0
|
||||||
? run("pnpm", ["exec", "vitest", "run", "tests/api/wp4-04-location.test.ts", "tests/api/wp6-03-services.test.ts"])
|
? run("pnpm", ["exec", "vitest", "run", "tests/api/wp4-04-location.test.ts", "tests/api/wp6-03-services.test.ts"])
|
||||||
: { command: "pnpm exec vitest run tests/api/wp4-04-location.test.ts tests/api/wp6-03-services.test.ts", exit_code: 1, finished_at: new Date().toISOString(), started_at: new Date().toISOString() };
|
: { command: "pnpm exec vitest run tests/api/wp4-04-location.test.ts tests/api/wp6-03-services.test.ts", exit_code: 1, finished_at: new Date().toISOString(), started_at: new Date().toISOString() };
|
||||||
const external = run("pnpm", ["validate:external", "--", "--service", "amap", "--run-id", runId]);
|
const supervisorBuild = run("dotnet", ["build", "supervisor/Dada.Supervisor/Dada.Supervisor.csproj", "--configuration", "Release"]);
|
||||||
|
const supervisorExe = resolve("supervisor", "Dada.Supervisor", "bin", "Release", "net8.0-windows", "Dada.Supervisor.exe");
|
||||||
|
const external = supervisorBuild.exit_code === 0 && existsSync(supervisorExe)
|
||||||
|
? run(supervisorExe, ["secrets", "probe", "api-amap"])
|
||||||
|
: { command: `${supervisorExe} secrets probe api-amap`, exit_code: 3, finished_at: new Date().toISOString(), started_at: new Date().toISOString(), stderr: "supervisor_probe_unavailable", stdout: "" };
|
||||||
const trace = run("pnpm", ["validate:tdd-trace"]);
|
const trace = run("pnpm", ["validate:tdd-trace"]);
|
||||||
const commands = [build, contract, external, trace].map(({ command, exit_code, finished_at, started_at }) => ({ command, exit_code, finished_at, started_at }));
|
const commands = [build, contract, external, trace].map(({ command, exit_code, finished_at, started_at }) => ({ command, exit_code, finished_at, started_at }));
|
||||||
const parsedExternal = (() => {
|
const parsedExternal = (() => {
|
||||||
try { return JSON.parse(external.stdout.trim()); } catch { return { status: "invalid_output" }; }
|
try { return JSON.parse(external.stdout.trim().split(/\r?\n/).at(-1) ?? ""); } catch { return { status: "invalid_output" }; }
|
||||||
})();
|
})();
|
||||||
const blocker = "real_amap_credentials_absent";
|
const probePassed = external.exit_code === 0 && parsedExternal.code === "amap_probe_passed" && parsedExternal.real_calls === 2;
|
||||||
|
const blocker = probePassed ? "manual_acceptance_required" : (parsedExternal.code ?? "real_amap_validation_failed");
|
||||||
const contractPassed = contract.exit_code === 0;
|
const contractPassed = contract.exit_code === 0;
|
||||||
const tracePassed = trace.exit_code === 0;
|
const tracePassed = trace.exit_code === 0;
|
||||||
writeFileSync(resolve(caseDirectory, "amap-contract.json"), `${JSON.stringify({
|
writeFileSync(resolve(caseDirectory, "amap-contract.json"), `${JSON.stringify({
|
||||||
checks: {
|
checks: {
|
||||||
location_and_reverse_geocode: "mock_contract_passed",
|
location_and_reverse_geocode: probePassed ? "real_probe_passed" : "not_verified",
|
||||||
qps_and_allowlist: "not_verified",
|
qps_and_allowlist: "not_verified",
|
||||||
security_binding: "not_verified",
|
security_binding: "not_verified",
|
||||||
dyn004_hard_stop: "mock_contract_passed",
|
dyn004_hard_stop: "contract_passed",
|
||||||
monthly_hard_limit_1000: "not_verified",
|
monthly_hard_limit_1000: "not_verified",
|
||||||
},
|
},
|
||||||
mode: "controlled_real",
|
mode: probePassed ? "controlled_real" : "blocked",
|
||||||
real_calls: 0,
|
real_calls: parsedExternal.real_calls ?? 0,
|
||||||
status: "externally_blocked",
|
status: "externally_blocked",
|
||||||
blocker,
|
blocker,
|
||||||
schema_version: "1.0",
|
schema_version: "1.0",
|
||||||
}, null, 2)}\n`);
|
}, null, 2)}\n`);
|
||||||
writeFileSync(resolve(caseDirectory, "external-calls.json"), `${JSON.stringify({
|
writeFileSync(resolve(caseDirectory, "external-calls.json"), `${JSON.stringify({
|
||||||
blocker,
|
blocker,
|
||||||
mode: parsedExternal.mode ?? "mock",
|
mode: probePassed ? "controlled_real" : "blocked",
|
||||||
real_calls: 0,
|
real_calls: parsedExternal.real_calls ?? 0,
|
||||||
service: "amap",
|
service: "amap",
|
||||||
source_command_status: parsedExternal.status ?? "unknown",
|
source_command_status: parsedExternal.code ?? "unknown",
|
||||||
status: "externally_blocked",
|
status: "externally_blocked",
|
||||||
schema_version: "1.0",
|
schema_version: "1.0",
|
||||||
}, null, 2)}\n`);
|
}, null, 2)}\n`);
|
||||||
@@ -69,7 +74,7 @@ writeFileSync(resolve(caseDirectory, "redaction.json"), `${JSON.stringify({
|
|||||||
}, null, 2)}\n`);
|
}, null, 2)}\n`);
|
||||||
writeFileSync(resolve(caseDirectory, "manual-review.json"), `${JSON.stringify({
|
writeFileSync(resolve(caseDirectory, "manual-review.json"), `${JSON.stringify({
|
||||||
blocker,
|
blocker,
|
||||||
required_before_release: ["controlled_amap_key", "security_binding", "allowlist_confirmation", "real_location_and_reverse_geocode", "qps_and_1000_call_hard_stop"],
|
required_before_release: probePassed ? ["security_binding", "allowlist_confirmation", "qps_and_1000_call_hard_stop"] : ["controlled_amap_key", "security_binding", "allowlist_confirmation", "real_location_and_reverse_geocode", "qps_and_1000_call_hard_stop"],
|
||||||
status: "externally_blocked",
|
status: "externally_blocked",
|
||||||
schema_version: "1.0",
|
schema_version: "1.0",
|
||||||
}, null, 2)}\n`);
|
}, null, 2)}\n`);
|
||||||
@@ -78,7 +83,7 @@ const result = {
|
|||||||
automation: ["controlled_real", "manual_review"],
|
automation: ["controlled_real", "manual_review"],
|
||||||
blocker,
|
blocker,
|
||||||
contract_regression: contractPassed ? "passed" : "failed",
|
contract_regression: contractPassed ? "passed" : "failed",
|
||||||
external_calls: 0,
|
external_calls: parsedExternal.real_calls ?? 0,
|
||||||
finished_at: new Date().toISOString(),
|
finished_at: new Date().toISOString(),
|
||||||
manifest: { path: "tasks.manifest.json", sha256: createHash("sha256").update(readFileSync("tasks.manifest.json")).digest("hex").toUpperCase() },
|
manifest: { path: "tasks.manifest.json", sha256: createHash("sha256").update(readFileSync("tasks.manifest.json")).digest("hex").toUpperCase() },
|
||||||
missing_evidence: [],
|
missing_evidence: [],
|
||||||
|
|||||||
@@ -0,0 +1,57 @@
|
|||||||
|
using System.Net;
|
||||||
|
using System.Text.Json;
|
||||||
|
|
||||||
|
namespace Dada.Supervisor;
|
||||||
|
|
||||||
|
internal static class AmapProbe
|
||||||
|
{
|
||||||
|
private static readonly HttpClient Client = new() { Timeout = TimeSpan.FromSeconds(15) };
|
||||||
|
|
||||||
|
internal static int Run(string? key)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(key))
|
||||||
|
{
|
||||||
|
Write("amap_credentials_missing", false, 0, null, null);
|
||||||
|
return 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var reverse = Call("https://restapi.amap.com/v3/geocode/regeo?location=120.6994,27.9943&extensions=base", key).GetAwaiter().GetResult();
|
||||||
|
var geocode = Call($"https://restapi.amap.com/v3/geocode/geo?address={Uri.EscapeDataString("北京市天安门")}", key).GetAwaiter().GetResult();
|
||||||
|
var success = reverse.Status == "1" && geocode.Status == "1";
|
||||||
|
Write(success ? "amap_probe_passed" : "amap_provider_rejected", success, 2, reverse.Status, geocode.Status);
|
||||||
|
return success ? 0 : 3;
|
||||||
|
}
|
||||||
|
catch (TaskCanceledException)
|
||||||
|
{
|
||||||
|
Write("amap_probe_timeout", false, 0, null, null);
|
||||||
|
return 3;
|
||||||
|
}
|
||||||
|
catch (HttpRequestException exception)
|
||||||
|
{
|
||||||
|
Write($"amap_probe_network_{exception.StatusCode?.ToString() ?? "error"}", false, 0, null, null);
|
||||||
|
return 3;
|
||||||
|
}
|
||||||
|
catch (JsonException)
|
||||||
|
{
|
||||||
|
Write("amap_probe_invalid_response", false, 0, null, null);
|
||||||
|
return 3;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async Task<ProbeResponse> Call(string endpoint, string key)
|
||||||
|
{
|
||||||
|
using var response = await Client.GetAsync($"{endpoint}&key={Uri.EscapeDataString(key)}");
|
||||||
|
response.EnsureSuccessStatusCode();
|
||||||
|
await using var stream = await response.Content.ReadAsStreamAsync();
|
||||||
|
using var document = await JsonDocument.ParseAsync(stream);
|
||||||
|
var root = document.RootElement;
|
||||||
|
return new ProbeResponse(root.GetProperty("status").GetString() ?? "", root.TryGetProperty("infocode", out var code) ? code.GetString() : null);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void Write(string code, bool success, int realCalls, string? reverseStatus, string? geocodeStatus) =>
|
||||||
|
Console.WriteLine(JsonSerializer.Serialize(new { code, success, real_calls = realCalls, reverse_status = reverseStatus, geocode_status = geocodeStatus }));
|
||||||
|
|
||||||
|
private sealed record ProbeResponse(string Status, string? InfoCode);
|
||||||
|
}
|
||||||
@@ -83,6 +83,8 @@ internal static class OfflineCommandRouter
|
|||||||
store.Write(target, value);
|
store.Write(target, value);
|
||||||
WriteResult("credential_saved", true);
|
WriteResult("credential_saved", true);
|
||||||
return 0;
|
return 0;
|
||||||
|
case "probe" when target == CredentialCatalog.ApiAmap:
|
||||||
|
return AmapProbe.Run(store.Read(target));
|
||||||
default:
|
default:
|
||||||
return Usage();
|
return Usage();
|
||||||
}
|
}
|
||||||
@@ -198,7 +200,7 @@ internal static class OfflineCommandRouter
|
|||||||
|
|
||||||
private static int Usage()
|
private static int Usage()
|
||||||
{
|
{
|
||||||
WriteResult("usage: configure init|data-root|asset-root; secrets set|status|clear; admin-allowlist add|remove|status; doctor", false);
|
WriteResult("usage: configure init|data-root|asset-root; secrets set|status|clear|probe; admin-allowlist add|remove|status; doctor", false);
|
||||||
return 2;
|
return 2;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user