chore(rebar): Pier_T_rebar 작업 폴더 초기 구성 및 외부 의존 파일 통합
- Code\rebar_pipeline, review_pipeline, pier-program을 정션에서 실제 폴더로 전환 - Code\rhino_mcp 및 CLAUDE_rhino배경.md 추가, .mcp.json은 PYTHONPATH로 새 위치 사용 - pier-program 내부 .git 제거(이력은 backup\pier-program_20261028.bundle에 보관) - worktree 가이드 반영(CLAUDE.local.md, HANDOFF.md) - D:\Rhino-MCP 경로 참조를 새 위치로 치환 Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
commit
964a841de1
3978 files changed
+58013356
No files matched your search
@@ -0,0 +1,81 @@
|
||||
$ErrorActionPreference = 'Stop'
|
||||
$admin = 'http://localhost:2019'
|
||||
$id = 'drawing-model-box-sample-deployment'
|
||||
$public = 'http://10.14.12.22/box-sample/'
|
||||
$created = $false
|
||||
function Test-StaticFallback($routes) {
|
||||
if (@($routes).Count -eq 0) { return $false }
|
||||
foreach ($r in $routes) {
|
||||
if (@($r.handle).Count -eq 0) { return $false }
|
||||
foreach ($h in $r.handle) {
|
||||
if ($h.handler -eq 'static_response') { continue }
|
||||
if ($h.handler -eq 'subroute' -and (Test-StaticFallback $h.routes)) { continue }
|
||||
return $false
|
||||
}
|
||||
}
|
||||
return $true
|
||||
}
|
||||
try {
|
||||
$response = Invoke-WebRequest -UseBasicParsing "$admin/config/"
|
||||
$config = $response.Content | ConvertFrom-Json
|
||||
if (-not $response.Headers['ETag']) { throw 'Caddy configuration ETag is required.' }
|
||||
$servers = @($config.apps.http.servers.PSObject.Properties | Where-Object {
|
||||
@($_.Value.listen | Where-Object { $_ -match '(^|:)80$' }).Count -gt 0
|
||||
})
|
||||
if ($servers.Count -ne 1) { throw 'Cannot uniquely identify the existing HTTP port 80 server.' }
|
||||
$server = $servers[0]
|
||||
$routes = @($server.Value.routes)
|
||||
$own = @($routes | Where-Object { $_.'@id' -eq $id })
|
||||
$route = @{
|
||||
'@id' = $id
|
||||
match = @(@{host=@('10.14.12.22'); path=@('/box-sample','/box-sample/*')})
|
||||
handle = @(@{handler='reverse_proxy'; upstreams=@(@{dial='127.0.0.1:5092'})})
|
||||
terminal = $true
|
||||
}
|
||||
if ($own.Count -gt 1) { throw 'Duplicate deployment route.' }
|
||||
if ($own.Count -eq 1) {
|
||||
if (@($own[0].handle).Count -ne 1 -or $own[0].handle[0].handler -ne 'reverse_proxy' -or
|
||||
@($own[0].handle[0].upstreams).Count -ne 1 -or $own[0].handle[0].upstreams[0].dial -ne '127.0.0.1:5092' -or
|
||||
(@($own[0].match[0].path) -join ',') -ne '/box-sample,/box-sample/*' -or
|
||||
(@($own[0].match[0].host) -join ',') -ne '10.14.12.22' -or -not $own[0].terminal) {
|
||||
throw 'Existing deployment route differs; refusing to overwrite it.'
|
||||
}
|
||||
} else {
|
||||
# Preserve every middleware, maintenance and service route in its original
|
||||
# order. Insert only before a proven static-only final fallback.
|
||||
$insertIndex = $routes.Count - 1
|
||||
if ($insertIndex -lt 0 -or $routes[$insertIndex].match -or
|
||||
-not (Test-StaticFallback @($routes[$insertIndex]))) {
|
||||
throw 'Expected a final unmatched static-only fallback; no settings changed.'
|
||||
}
|
||||
foreach ($existing in $routes) {
|
||||
$json = $existing | ConvertTo-Json -Depth 100 -Compress
|
||||
# Do not bypass a shared authentication policy or shadow an existing mount.
|
||||
if ($json -match 'authentication|http_basic|forward_auth|/box-sample') {
|
||||
throw 'Existing route/authentication needs administrator review; nothing was replaced.'
|
||||
}
|
||||
}
|
||||
$backup = Join-Path $PSScriptRoot ('caddy-before-box-sample-' + (Get-Date -Format 'yyyyMMdd-HHmmss-fff') + '.json')
|
||||
[IO.File]::WriteAllText($backup, $response.Content)
|
||||
$name = [Uri]::EscapeDataString($server.Name)
|
||||
if ($null -eq $server.Value.routes) { throw 'HTTP routes array missing; no automatic replacement allowed.' }
|
||||
$body = $route | ConvertTo-Json -Depth 30 -Compress
|
||||
Invoke-WebRequest -UseBasicParsing -Method Put -Uri "$admin/config/apps/http/servers/$name/routes/$insertIndex" -Headers @{'If-Match'=$response.Headers['ETag']} -ContentType 'application/json' -Body $body | Out-Null
|
||||
$created = $true
|
||||
}
|
||||
foreach ($path in @('', 'api/status', 'src/app.js')) {
|
||||
$check = Invoke-WebRequest -UseBasicParsing -Uri ($public+$path) -TimeoutSec 30
|
||||
if ($check.StatusCode -ne 200 -or $check.Headers['X-DMS-Deployment'] -ne 'box-sample') { throw "Public route validation failed: $path" }
|
||||
if ($path -eq 'api/status' -and -not ($check.Content | ConvertFrom-Json).ok) { throw 'API status failed.' }
|
||||
if ($path -eq 'src/app.js' -and $check.Content -notmatch '/box-sample/api/status') { throw 'Frontend API prefix missing.' }
|
||||
}
|
||||
Write-Host "VERIFIED: $public (page, API, JavaScript)."
|
||||
Write-Host 'Caddy API changes are autosaved; reboot persistence requires Caddy to resume that configuration.'
|
||||
} catch {
|
||||
if ($created) {
|
||||
# Remove only this newly inserted route; never restore over unrelated changes.
|
||||
try { Invoke-WebRequest -UseBasicParsing -Method Delete -Uri "$admin/id/$id" | Out-Null } catch { Write-Warning 'Could not roll back this deployment route.' }
|
||||
}
|
||||
Write-Error $_
|
||||
exit 1
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
@echo off
|
||||
setlocal
|
||||
echo Connecting /box-sample without rebuilding or deleting model data.
|
||||
powershell.exe -NoProfile -ExecutionPolicy Bypass -File "%~dp0configure-caddy.ps1"
|
||||
if errorlevel 1 goto failed
|
||||
start "" "http://10.14.12.22/box-sample/"
|
||||
echo SUCCESS: service address verified.
|
||||
pause
|
||||
exit /b 0
|
||||
:failed
|
||||
echo FAILED: Please send the error shown above. No model data was deleted.
|
||||
pause
|
||||
exit /b 1
|
||||
@@ -0,0 +1,60 @@
|
||||
"""Import an owner-authorized ChatGPT login over stdin into a private volume.
|
||||
|
||||
Never print credentials or replace a refreshed server login with an old copy.
|
||||
This script has no HTTP endpoint and is invoked by the server operator only.
|
||||
"""
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import sys
|
||||
import tempfile
|
||||
|
||||
LIMIT=1024*1024
|
||||
|
||||
|
||||
def parse_auth(payload):
|
||||
if not payload or len(payload)>LIMIT: raise ValueError('Invalid AI authentication file size')
|
||||
try: value=json.loads(payload)
|
||||
except (ValueError,UnicodeError): raise ValueError('Invalid AI authentication JSON') from None
|
||||
if not isinstance(value,dict) or value.get('auth_mode') not in (None,'chatgpt'):
|
||||
raise ValueError('ChatGPT authentication is required')
|
||||
tokens=value.get('tokens')
|
||||
if not isinstance(tokens,dict) or any(not isinstance(tokens.get(k),str) or not tokens[k].strip()
|
||||
for k in ('access_token','refresh_token','id_token','account_id')):
|
||||
raise ValueError('Incomplete ChatGPT authentication')
|
||||
return value
|
||||
|
||||
|
||||
def install(payload, directory):
|
||||
incoming=parse_auth(payload); directory=Path(directory)
|
||||
if directory.is_symlink(): raise ValueError('AI authentication directory must not be a symlink')
|
||||
directory.mkdir(mode=0o700,parents=True,exist_ok=True);directory.chmod(0o700)
|
||||
target=directory/'auth.json'
|
||||
if target.is_symlink(): raise ValueError('AI authentication file must not be a symlink')
|
||||
if target.exists():
|
||||
existing=parse_auth(target.read_bytes())
|
||||
if existing['tokens']['account_id']!=incoming['tokens']['account_id']:
|
||||
raise ValueError('A different server account exists; it was not overwritten')
|
||||
target.chmod(0o600)
|
||||
return 'Existing server login preserved (including refreshed credentials)'
|
||||
descriptor,temporary=tempfile.mkstemp(prefix='.auth-import-',dir=directory)
|
||||
try:
|
||||
with os.fdopen(descriptor,'wb') as stream:
|
||||
stream.write(json.dumps(incoming).encode());stream.flush();os.fsync(stream.fileno())
|
||||
try: os.link(temporary,target)
|
||||
except FileExistsError: return install(payload,directory)
|
||||
finally: Path(temporary).unlink(missing_ok=True)
|
||||
return 'ChatGPT login installed in the private server volume'
|
||||
|
||||
|
||||
def main():
|
||||
try:
|
||||
print(install(sys.stdin.buffer.read(LIMIT+1),Path.home()/'.codex'))
|
||||
except (ValueError,OSError) as error:
|
||||
# Messages are fixed strings, never JSON, token values or stdin.
|
||||
print(str(error) if isinstance(error,ValueError) else 'Cannot install AI authentication',file=sys.stderr)
|
||||
return 1
|
||||
return 0
|
||||
|
||||
|
||||
if __name__=='__main__':sys.exit(main())
|
||||
@@ -0,0 +1,46 @@
|
||||
"""Install only the same pinned DWG decoder used on the verified local host.
|
||||
|
||||
Build-time only. No drawings, credentials or model generation are involved.
|
||||
"""
|
||||
import hashlib
|
||||
from pathlib import Path
|
||||
import platform
|
||||
import shutil
|
||||
import tarfile
|
||||
import tempfile
|
||||
import urllib.request
|
||||
|
||||
URL = ('https://conda.anaconda.org/conda-forge/linux-64/'
|
||||
'libredwg-0.11.3876-py38pl5321hf1fe3a4_2.tar.bz2')
|
||||
SHA256 = '23e82c10d5e09b1444ca099c3710893f363d67a98417b0b69afd7155b9759f21'
|
||||
|
||||
|
||||
def install(destination=Path('/opt/libredwg')):
|
||||
if platform.machine() not in ('x86_64', 'AMD64'):
|
||||
raise RuntimeError('This pinned converter requires a linux/amd64 Docker host')
|
||||
members = {'bin/dwg2dxf': 'bin/dwg2dxf',
|
||||
'lib/libredwg.so.0.0.11': 'lib/libredwg.so.0.0.11',
|
||||
'info/licenses/COPYING': 'licenses/COPYING',
|
||||
'info/recipe/meta.yaml': 'licenses/package-source.yaml'}
|
||||
with tempfile.TemporaryDirectory(prefix='dwg-runtime-') as tmp:
|
||||
archive = Path(tmp) / 'runtime.tar.bz2'
|
||||
with urllib.request.urlopen(URL, timeout=120) as response, archive.open('wb') as out:
|
||||
shutil.copyfileobj(response, out)
|
||||
with archive.open('rb') as stream:
|
||||
if hashlib.file_digest(stream, 'sha256').hexdigest() != SHA256:
|
||||
raise ValueError('DWG runtime checksum mismatch')
|
||||
with tarfile.open(archive) as bundle:
|
||||
for source, relative in members.items():
|
||||
member = bundle.getmember(source)
|
||||
if not member.isfile():
|
||||
raise ValueError('Expected regular DWG runtime file')
|
||||
target = destination / relative
|
||||
target.parent.mkdir(parents=True, exist_ok=True)
|
||||
with bundle.extractfile(member) as inp, target.open('xb') as out:
|
||||
shutil.copyfileobj(inp, out)
|
||||
target.chmod(0o755 if relative.startswith(('bin/', 'lib/')) else 0o644)
|
||||
(destination / 'lib/libredwg.so.0').symlink_to('libredwg.so.0.0.11')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
install()
|
||||
@@ -0,0 +1,84 @@
|
||||
"""Deployment-only HTTP prefix adapter; modeling code remains unchanged."""
|
||||
import http.client
|
||||
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
from urllib.parse import urlsplit
|
||||
|
||||
PREFIX = '/box-sample'
|
||||
UPSTREAM_PORT = 8097
|
||||
HOP = {'connection', 'transfer-encoding', 'keep-alive', 'upgrade',
|
||||
'proxy-authenticate', 'proxy-authorization', 'te', 'trailer'}
|
||||
ROOT_URL = re.compile(rb'''(["'`])/(api/|src/|dxf_rasterizer\.html|model-report\.html)''')
|
||||
|
||||
|
||||
def rewrite(payload):
|
||||
return ROOT_URL.sub(lambda m: m[1] + PREFIX.encode() + b'/' + m[2], payload)
|
||||
|
||||
|
||||
class Gateway(BaseHTTPRequestHandler):
|
||||
def forward(self):
|
||||
parsed = urlsplit(self.path)
|
||||
if parsed.path == PREFIX:
|
||||
self.send_response(308)
|
||||
self.send_header('Location', PREFIX + '/' + ('?' + parsed.query if parsed.query else ''))
|
||||
self.send_header('Content-Length', '0')
|
||||
self.end_headers()
|
||||
return
|
||||
if not parsed.path.startswith(PREFIX + '/'):
|
||||
self.send_error(404)
|
||||
return
|
||||
if self.headers.get('Transfer-Encoding'):
|
||||
self.send_error(411, 'Content-Length required')
|
||||
return
|
||||
target = self.path[len(PREFIX):]
|
||||
connection = http.client.HTTPConnection('127.0.0.1', UPSTREAM_PORT, timeout=600)
|
||||
try:
|
||||
size = int(self.headers.get('Content-Length', '0'))
|
||||
if size < 0 or size > 512 * 1024 * 1024:
|
||||
self.send_error(413)
|
||||
return
|
||||
headers = {k: v for k, v in self.headers.items()
|
||||
if k.lower() not in HOP | {'accept-encoding', 'host'}}
|
||||
headers['Host'] = '127.0.0.1:' + str(UPSTREAM_PORT)
|
||||
connection.request(self.command, target, self.rfile.read(size) if size else None, headers)
|
||||
response = connection.getresponse()
|
||||
content_type = response.getheader('Content-Type', '').lower()
|
||||
text = any(t in content_type for t in ('json', 'javascript', 'text/html', 'text/css'))
|
||||
body = rewrite(response.read()) if text and self.command != 'HEAD' else None
|
||||
self.send_response(response.status)
|
||||
for key, value in response.getheaders():
|
||||
if key.lower() in HOP | {'content-length', 'etag', 'content-md5'}:
|
||||
continue
|
||||
if key.lower() == 'location' and value.startswith('/') and not value.startswith('//'):
|
||||
value = PREFIX + value
|
||||
self.send_header(key, value)
|
||||
self.send_header('X-DMS-Deployment', 'box-sample')
|
||||
self.send_header('Cache-Control', 'no-store')
|
||||
if body is not None:
|
||||
self.send_header('Content-Length', str(len(body)))
|
||||
elif response.getheader('Content-Length') is not None:
|
||||
self.send_header('Content-Length', response.getheader('Content-Length'))
|
||||
self.end_headers()
|
||||
if self.command != 'HEAD':
|
||||
if body is not None:
|
||||
self.wfile.write(body)
|
||||
else:
|
||||
while chunk := response.read(65536):
|
||||
self.wfile.write(chunk)
|
||||
except (ValueError, OSError, http.client.HTTPException):
|
||||
self.send_error(502, 'Application not ready')
|
||||
finally:
|
||||
connection.close()
|
||||
|
||||
do_GET = do_POST = do_HEAD = do_PUT = do_DELETE = forward
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
child = subprocess.Popen([sys.executable, 'src/backend/serve.py', '--host', '127.0.0.1', '--port', str(UPSTREAM_PORT)])
|
||||
try:
|
||||
ThreadingHTTPServer(('0.0.0.0', 8080), Gateway).serve_forever()
|
||||
finally:
|
||||
child.terminate()
|
||||
child.wait(timeout=15)
|
||||
@@ -0,0 +1,16 @@
|
||||
$ErrorActionPreference = 'Stop'
|
||||
$tokens = $null
|
||||
$errors = $null
|
||||
$ast = [System.Management.Automation.Language.Parser]::ParseFile(
|
||||
(Join-Path $PSScriptRoot 'configure-caddy.ps1'), [ref]$tokens, [ref]$errors)
|
||||
if ($errors.Count) { throw ($errors | Out-String) }
|
||||
$fn = $ast.Find({param($n) $n -is [System.Management.Automation.Language.FunctionDefinitionAst]}, $true)
|
||||
Invoke-Expression $fn.Extent.Text
|
||||
$good = '{"handle":[{"handler":"subroute","routes":[{"match":[{"path":["/"]}],"handle":[{"handler":"static_response"}]},{"handle":[{"handler":"static_response"}]}]}]}' | ConvertFrom-Json
|
||||
if (-not (Test-StaticFallback @($good))) { throw 'Static fallback rejected' }
|
||||
$good.handle[0].routes[1].handle[0].handler = 'authentication'
|
||||
if (Test-StaticFallback @($good)) { throw 'Authentication fallback accepted' }
|
||||
$good.handle[0].routes[1].handle[0].handler = 'reverse_proxy'
|
||||
if (Test-StaticFallback @($good)) { throw 'Proxy fallback accepted' }
|
||||
if (Test-StaticFallback @()) { throw 'Empty fallback accepted' }
|
||||
Write-Output 'PASS: syntax, nested static fallback, authentication/proxy/empty guards'
|
||||
@@ -0,0 +1,43 @@
|
||||
import json
|
||||
from pathlib import Path
|
||||
import tempfile
|
||||
import unittest
|
||||
from install_ai_auth import install,parse_auth
|
||||
|
||||
|
||||
def sample(account='owner',refresh='fake-refresh'):
|
||||
return json.dumps({'auth_mode':'chatgpt','tokens':{'account_id':account,
|
||||
'access_token':'fake-access','id_token':'fake-id','refresh_token':refresh}}).encode()
|
||||
|
||||
|
||||
class ImportTests(unittest.TestCase):
|
||||
def test_install_private_file_without_printing_credentials(self):
|
||||
with tempfile.TemporaryDirectory() as temporary:
|
||||
root=Path(temporary)/'login';message=install(sample(),root)
|
||||
self.assertNotIn('fake',message)
|
||||
self.assertEqual((root/'auth.json').stat().st_mode & 0o777,0o600)
|
||||
self.assertEqual(root.stat().st_mode & 0o777,0o700)
|
||||
self.assertEqual(parse_auth((root/'auth.json').read_bytes()),parse_auth(sample()))
|
||||
|
||||
def test_refresh_preserved_and_different_account_not_overwritten(self):
|
||||
with tempfile.TemporaryDirectory() as temporary:
|
||||
root=Path(temporary)/'login';install(sample(refresh='newer-refresh'),root)
|
||||
before=(root/'auth.json').read_bytes();install(sample(),root)
|
||||
self.assertEqual((root/'auth.json').read_bytes(),before)
|
||||
with self.assertRaises(ValueError):install(sample('different'),root)
|
||||
self.assertEqual((root/'auth.json').read_bytes(),before)
|
||||
|
||||
def test_invalid_inputs_do_not_create_auth_directory(self):
|
||||
for payload in (b'',b'not json',b'[]',b'{}',b'x'*(1024*1024+1)):
|
||||
with tempfile.TemporaryDirectory() as temporary:
|
||||
root=Path(temporary)/'login'
|
||||
with self.assertRaises(ValueError):install(payload,root)
|
||||
self.assertFalse(root.exists())
|
||||
|
||||
def test_symlink_is_rejected(self):
|
||||
with tempfile.TemporaryDirectory() as temporary:
|
||||
root=Path(temporary);(root/'link').symlink_to(root/'elsewhere')
|
||||
with self.assertRaises(ValueError):install(sample(),root/'link')
|
||||
|
||||
|
||||
if __name__=='__main__':unittest.main()
|
||||
@@ -0,0 +1,21 @@
|
||||
$ErrorActionPreference='Stop'
|
||||
$launcher=Get-Content (Join-Path $PSScriptRoot '../deploy-server.bat')
|
||||
$line=@($launcher | Where-Object { $_ -like 'powershell *' -and $_ -like '*$lines=@(& git*' })
|
||||
if ($line.Count -ne 1) { throw 'Origin verification command missing' }
|
||||
$command=$line[0].Substring($line[0].IndexOf('-Command "')+10).TrimEnd('"')
|
||||
$env:DMS_REPOSITORY='https://gitea.hmac.kr/is-div/design-model-box-sample_2.git'
|
||||
$env:DMS_DEPLOY_SOURCE='C:\folder with spaces\deploy-source'
|
||||
$cases=@(
|
||||
@{url=$env:DMS_REPOSITORY;code=0;expected=0},
|
||||
@{url=($env:DMS_REPOSITORY+"`r`n");code=0;expected=0},
|
||||
@{url='https://example.invalid/other.git';code=0;expected=4},
|
||||
@{url='';code=128;expected=2}
|
||||
)
|
||||
foreach ($case in $cases) {
|
||||
$env:TEST_ORIGIN=$case.url
|
||||
$env:TEST_GIT_CODE=[string]$case.code
|
||||
$mock='function git { $global:LASTEXITCODE=[int]$env:TEST_GIT_CODE; if ($env:TEST_ORIGIN) { $env:TEST_ORIGIN } }; '
|
||||
& (Join-Path $PSHOME 'pwsh') -NoProfile -Command ($mock+$command)
|
||||
if ($LASTEXITCODE -ne $case.expected) { throw "Unexpected exit: $LASTEXITCODE" }
|
||||
}
|
||||
Write-Host 'PASS: exact origin, line endings, different repository, Git error'
|
||||
@@ -0,0 +1,50 @@
|
||||
"""Offline container smoke check: seed cache, filename normalization and DWG decode.
|
||||
|
||||
Run with a read-only existing DWG mounted at /test-input.dwg. No AI calls.
|
||||
"""
|
||||
import hashlib
|
||||
import json
|
||||
from pathlib import Path
|
||||
import re
|
||||
import sys
|
||||
import tempfile
|
||||
sys.path.insert(0, '/app/src/backend')
|
||||
from shared_cache_seed import install_bundle
|
||||
from model_snapshot_lookup import job_drawings, lookup
|
||||
from dwg_conversion import convert_dwg
|
||||
|
||||
|
||||
def main():
|
||||
source = Path('/test-input.dwg')
|
||||
raw_hash = hashlib.sha256(source.read_bytes()).hexdigest()
|
||||
with tempfile.TemporaryDirectory(prefix='packaged-dwg-test-') as tmp:
|
||||
data = Path(tmp)
|
||||
install_bundle(Path('/app/shared-cache/selected-models.zip'), data)
|
||||
jobs = data / '8097-jobs'
|
||||
matches = []
|
||||
for file in jobs.glob('*.json'):
|
||||
job = json.loads(file.read_text())
|
||||
drawings = job_drawings(job, data / 'uploads')
|
||||
hit = lookup(jobs, drawings, data / 'uploads')
|
||||
assert hit and hit['jobId'] == file.stem
|
||||
renamed = [dict(row, name=re.sub(r'_(\d\d)_', r'(\1)', row['name'])) for row in drawings]
|
||||
assert lookup(jobs, renamed, data / 'uploads')['jobId'] == file.stem
|
||||
if len(drawings) > 1:
|
||||
assert lookup(jobs, renamed[:-1], data / 'uploads') is None
|
||||
changed = [dict(row) for row in drawings]
|
||||
changed[0]['sha256'] = 'f' * 64
|
||||
assert lookup(jobs, changed, data / 'uploads') is None
|
||||
matches.extend(row for row in drawings if row['sha256'] == raw_hash)
|
||||
assert matches, 'Input DWG does not correspond to a retained model'
|
||||
audit = convert_dwg(source, data / 'decoded.dxf')
|
||||
expected = matches[0]['conversionAudit']
|
||||
assert audit['source_sha256'] == raw_hash
|
||||
assert audit['entity_count'] == expected['entity_count'], (audit['entity_count'], expected['entity_count'])
|
||||
print(json.dumps({'cached_models_checked': len(list(jobs.glob('*.json'))),
|
||||
'dwg_entity_count': audit['entity_count'],
|
||||
'dxf_bytes_equal_to_saved_conversion': audit['output_sha256'] == expected['output_sha256'],
|
||||
'ai_calls': 0}))
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -0,0 +1,66 @@
|
||||
import http.client
|
||||
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
||||
import threading
|
||||
import unittest
|
||||
import subpath_gateway as gateway
|
||||
|
||||
|
||||
class Backend(BaseHTTPRequestHandler):
|
||||
def do_GET(self):
|
||||
if self.path=='/api/status':
|
||||
data=b'{"ok":true,"asset":"/api/uploads/file","unchanged":"/tmp/model.json"}'
|
||||
mime='application/json'
|
||||
elif self.path=='/src/app.js':
|
||||
data=b"fetch('/api/status'); frame.src=`/dxf_rasterizer.html?file=x`;"
|
||||
mime='application/javascript'
|
||||
elif self.path=='/api/uploads/file':data=b'\x00\xff\x01';mime='application/octet-stream'
|
||||
else:data=b'<link href="styles.css">';mime='text/html'
|
||||
self.send_response(200);self.send_header('Content-Type',mime)
|
||||
self.send_header('Content-Length',str(len(data)));self.end_headers();self.wfile.write(data)
|
||||
|
||||
def do_POST(self):
|
||||
body=self.rfile.read(int(self.headers['Content-Length']))
|
||||
self.send_response(200);self.send_header('Content-Length',str(len(body)))
|
||||
self.end_headers();self.wfile.write(body)
|
||||
|
||||
def log_message(self,*args):pass
|
||||
|
||||
|
||||
class GatewayTests(unittest.TestCase):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
cls.backend=ThreadingHTTPServer(('127.0.0.1',0),Backend)
|
||||
gateway.UPSTREAM_PORT=cls.backend.server_port
|
||||
cls.proxy=ThreadingHTTPServer(('127.0.0.1',0),gateway.Gateway)
|
||||
for s in (cls.backend,cls.proxy):threading.Thread(target=s.serve_forever,daemon=True).start()
|
||||
|
||||
@classmethod
|
||||
def tearDownClass(cls):
|
||||
for s in (cls.proxy,cls.backend):s.shutdown();s.server_close()
|
||||
|
||||
def request(self,path,method='GET',body=None):
|
||||
c=http.client.HTTPConnection('127.0.0.1',self.proxy.server_port)
|
||||
c.request(method,path,body);r=c.getresponse();result=(r.status,dict(r.getheaders()),r.read());c.close();return result
|
||||
|
||||
def test_mount_redirect_preserves_job(self):
|
||||
code,headers,_=self.request('/box-sample?job=abc')
|
||||
self.assertEqual(code,308);self.assertEqual(headers['Location'],'/box-sample/?job=abc')
|
||||
|
||||
def test_api_and_javascript_prefix_without_model_path_change(self):
|
||||
_,headers,body=self.request('/box-sample/api/status')
|
||||
self.assertIn(b'/box-sample/api/uploads/file',body);self.assertIn(b'/tmp/model.json',body)
|
||||
self.assertEqual(int(headers['Content-Length']),len(body))
|
||||
_,_,script=self.request('/box-sample/src/app.js')
|
||||
self.assertIn(b"fetch('/box-sample/api/status')",script)
|
||||
self.assertIn(b'/box-sample/dxf_rasterizer.html',script)
|
||||
|
||||
def test_binary_and_post_untouched(self):
|
||||
self.assertEqual(self.request('/box-sample/api/uploads/file')[2],b'\x00\xff\x01')
|
||||
self.assertEqual(self.request('/box-sample/api/analyze','POST',b'{"drawing":"/api/original"}')[2],b'{"drawing":"/api/original"}')
|
||||
|
||||
def test_other_service_paths_rejected(self):
|
||||
for path in ('/api/status','/box-sample-other/api/status'):
|
||||
self.assertEqual(self.request(path)[0],404)
|
||||
|
||||
|
||||
if __name__=='__main__':unittest.main()
|
||||
Reference in new issue
Block a user